diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f73bfa7927..c3ef9fa088 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index bf565453a5..2f4af38cca 100644 --- a/.gitignore +++ b/.gitignore @@ -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 + diff --git a/Dockerfile.build b/Dockerfile.build index 3ddff59d12..89f8c25101 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -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 diff --git a/README.md b/README.md index 3364c28284..818ed7142f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ [](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html)     [](https://www.privacyguides.org/en/real-time-communication/#simplex-chat)     [](https://www.whonix.org/wiki/Chat#Recommendation)     [](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. diff --git a/apps/ios/CODE.md b/apps/ios/CODE.md new file mode 100644 index 0000000000..adb5ef8c42 --- /dev/null +++ b/apps/ios/CODE.md @@ -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 4–6. + +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 | diff --git a/apps/ios/README.md b/apps/ios/README.md index de6c52c01d..fb6a6ed40d 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -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()` - Send typed commands and parse responses +- `recvSimpleXMsg()` - 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` diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 3f6998c9ec..0a401f9bf3 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -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 diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 7adf7a0435..a6896fa51d 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -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 { diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 193b675a57..f82a2fd2eb 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -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) diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index 25eab6c69e..aa4dfa24f8 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -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") diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index f1f4e686bd..46e9df1ef8 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -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] diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 79f4ef2f09..c6c6e88d8c 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -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)) } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5a042a6252..7eb2de11ab 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -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(bgDelay: Double? = nil, f: @escaping () -> T) -> T { return r } +// Spec: spec/api.md#chatSendCmdSync @inline(__always) func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) throws -> R { let res: APIResult = chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log) return try apiResult(res) } +// Spec: spec/api.md#chatApiSendCmdSync func chatApiSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) -> APIResult { if log { logger.debug("chatSendCmd \(cmd.cmdType)") @@ -112,12 +116,14 @@ func chatApiSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = tru return resp } +// Spec: spec/api.md#chatSendCmd @inline(__always) func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async throws -> R { let res: APIResult = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log) return try apiResult(res) } +// Spec: spec/api.md#chatApiSendCmdWithRetry func chatApiSendCmdWithRetry(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, inProgress: BoxedValue? = nil, retryNum: Int32 = 0) async -> APIResult? { let r: APIResult = 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(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) async -> APIResult { await withCheckedContinuation { cont in @@ -226,6 +233,7 @@ func apiResult(_ res: APIResult) throws -> R { } } +// Spec: spec/api.md#chatRecvMsg func chatRecvMsg(_ ctrl: chat_ctrl? = nil) async -> APIResult? { await withCheckedContinuation { cont in _ = withBGTask(bgDelay: msgDelay) { () -> APIResult? 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? 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)") diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index e1a6bb61e8..1e9a97c31b 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -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 diff --git a/apps/ios/Shared/Theme/Theme.swift b/apps/ios/Shared/Theme/Theme.swift index 3bd8f00c25..1f98b23a1d 100644 --- a/apps/ios/Shared/Theme/Theme.swift +++ b/apps/ios/Shared/Theme/Theme.swift @@ -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 } diff --git a/apps/ios/Shared/Theme/ThemeManager.swift b/apps/ios/Shared/Theme/ThemeManager.swift index 4166619d04..b9a35163cf 100644 --- a/apps/ios/Shared/Theme/ThemeManager.swift +++ b/apps/ios/Shared/Theme/ThemeManager.swift @@ -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 }) diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index ab7a47b944..754bcb2715 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -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) { diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index 1f28180e87..9df0c2f0b7 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -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 } diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift index db7910836e..2ce04e4b80 100644 --- a/apps/ios/Shared/Views/Call/WebRTCClient.swift +++ b/apps/ios/Shared/Views/Call/WebRTCClient.swift @@ -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() diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index ad82af05e2..c17d8e23a8 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift index 30f5e7a589..93ffb9f042 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift index 0283e9c07e..e5f3c05eed 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift index b2b4441646..5521470d07 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift index 1375b87a5a..49a086d45a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift index 67f7b69e2c..dcd6ea579c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 1b9376b5db..639de1dbc9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 3fcf578875..ddb58fdfd1 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index d1f49f635a..8b5172eccf 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift index 5e9fa691de..80cccbf907 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift @@ -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? diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index f07e90b953..a09518ffdb 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift index 2898a318a9..4719c3dcdc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index fc73778239..e3bc654ac9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 3201332c1e..ec23dc15a4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index eacbe9360a..80bea997d3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 47aee2a586..820074542f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift index ed2340b6c4..fb5d36ab12 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift index 250d9d5636..04f36c97a4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index 0b6f249b9c..123f7289bb 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index c9c9952688..ec8bc852c0 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift index f243a83142..e14683684d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index 47a30f6cf3..fdf3743aac 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index c6a5d0353c..953f4e8c82 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 2a1b526893..77bd41c5b8 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -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) diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 87c6ba92f8..3858d15252 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 5f48c18881..f72bf083f6 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -38,6 +38,7 @@ extension EnvironmentValues { } } +// Spec: spec/client/chat-view.md#ChatItemView struct ChatItemView: View { @ObservedObject var chat: Chat @ObservedObject var im: ItemsModel diff --git a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift index 93ecf870eb..9987fb4697 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift @@ -15,6 +15,7 @@ func apiLoadMessages( _ chatId: ChatId, _ im: ItemsModel, _ pagination: ChatPagination, + _ contentTag: MsgContentTag? = nil, _ search: String = "", _ openAroundItemId: ChatItem.ID? = nil, _ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange = { 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 diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 709758655f..057bf7f75f 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -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) -> Array { 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 = [.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() diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 3745d0f0b8..2c462df9e4 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -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)) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 07cd61583b..713f462c27 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 3154f16f5b..6b18c0c5ef 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 96b5e2898a..4113b75d0a 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -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" diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index bc1ac4ab65..43bc26e8f8 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 207c2170a3..135efae74f 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -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 { diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift index 75a6840c4e..3dc27c08f6 100644 --- a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift @@ -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" diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 4937bca20e..381057db5b 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -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) diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index efaba518a9..d84fa29c81 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -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: diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index be2c456802..112e4099c0 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -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 diff --git a/apps/ios/Shared/Views/ChatList/TagListView.swift b/apps/ios/Shared/Views/ChatList/TagListView.swift index 79d122eabf..f484ce8938 100644 --- a/apps/ios/Shared/Views/ChatList/TagListView.swift +++ b/apps/ios/Shared/Views/ChatList/TagListView.swift @@ -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 diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index b1cd4015c6..63d28e3624 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -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 diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift index 441a164f8a..dbc25e536f 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index 02a1b87826..9610b4a24d 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index a7e61b3105..d5d70abaea 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index 79c0a42ae0..76bdc898d5 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -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 diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift index c21ff9be8b..36608c58d6 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -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 diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift index 4a6f8e7549..6df31b4d59 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift @@ -5,6 +5,7 @@ // Created by Evgeny on 10/04/2023. // Copyright © 2023 SimpleX Chat. All rights reserved. // +// Spec: spec/architecture.md import SwiftUI diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift index ca30fa5ce8..046a3fd1fc 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift @@ -5,6 +5,7 @@ // Created by Evgeny on 11/04/2023. // Copyright © 2023 SimpleX Chat. All rights reserved. // +// Spec: spec/architecture.md import SwiftUI diff --git a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift index 7ec3ee1a42..995b9f5b0d 100644 --- a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index 0af8fa7ad8..2ff376701c 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -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 diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index 93fe19cf33..a28acfcba1 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -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 diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 3de1fdb972..71a155949b 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -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, diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index c9054f30da..2b38065bd9 100644 --- a/apps/ios/Shared/Views/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/NewChat/QRCode.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift index c8d0faafa7..f22d59fcac 100644 --- a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift +++ b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 33ffa04a50..b5598c1f85 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index f119beec50..7301c0421d 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index 03b0fcba1a..ab84bed7df 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index 7452d74e91..263b55a42d 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index 8f448dc508..daef95fbc6 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 31865e7af9..717405b03b 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 9f41a37b1d..80f35c1190 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -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 diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 916e3f9e78..8a7ab465d4 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index 02dec5a618..54a60eed19 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -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(_ string: String) -> T? { do { return try YAMLDecoder().decode(T.self, from: string) @@ -1150,6 +1156,7 @@ private func decodeYAML(_ 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) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift index 3a536c7b17..74d38b050b 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift index 1e38b7d5ec..6f76e69182 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 6f4710396a..64e3d15de0 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift index c8cb2349e7..b44271bd89 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index afbccc109c..abd8be03b9 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift index 97bfd360cb..97bf9ebc93 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift index b9737914ec..49e1ff79ea 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift index b28b1a4d1e..fd29fd906e 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index cb6fdf8597..c091224098 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -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 diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index ddfe59e719..ad3b5cdf95 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -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 diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index a59179ddfa..3c21db94b6 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -792,6 +792,10 @@ swipe action Всички членове на групата ще останат свързани. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Всички съобщения и файлове се изпращат с **криптиране от край до край**, с постквантова сигурност в директните съобщения. @@ -1142,6 +1146,10 @@ swipe action Аудио и видео разговори No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Аудио/видео разговори @@ -2552,6 +2560,14 @@ swipe action Изтрий съобщението на члена? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Изтрий съобщението? @@ -2560,7 +2576,8 @@ swipe action Delete messages Изтрий съобщенията - alert button + alert action +alert button Delete messages after @@ -3741,6 +3758,10 @@ snd error text Файловете и медията са забранени! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Филтрирайте непрочетените и любимите чатове. @@ -4194,6 +4215,10 @@ Error: %2$@ Изображението ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Веднага @@ -4442,6 +4467,10 @@ More improvements are coming soon! Покани приятели No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Покани членове @@ -4658,6 +4687,10 @@ This is your link for group %@! Запомнени настолни устройства No comment provided by engineer. + + Links + No comment provided by engineer. + List swipe action @@ -4773,6 +4806,10 @@ This is your link for group %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports chat feature @@ -4793,12 +4830,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Членът ще бъде премахнат от групата - това не може да бъде отменено! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6296,7 +6333,11 @@ swipe action Remove Премахване - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6318,7 +6359,7 @@ swipe action Remove member? Острани член? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6703,11 +6744,31 @@ chat item action Лентата за търсене приема линк за връзка. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Търсене или поставяне на SimpleX линк No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary No comment provided by engineer. @@ -8432,6 +8493,10 @@ To connect, please ask your contact to create another connection link and check Видеото ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Видео и файлове до 1gb diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index ace3079550..db9c2e1910 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -782,6 +782,10 @@ swipe action Všichni členové skupiny zůstanou připojeni. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. No comment provided by engineer. @@ -1117,6 +1121,10 @@ swipe action Hlasové a video hovory No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Audio/video hovory @@ -2438,6 +2446,14 @@ swipe action Smazat zprávu člena? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Smazat zprávu? @@ -2446,7 +2462,8 @@ swipe action Delete messages Smazat zprávy - alert button + alert action +alert button Delete messages after @@ -3596,6 +3613,10 @@ snd error text Soubory a média jsou zakázány! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Filtrovat nepřečtené a oblíbené chaty. @@ -4037,6 +4058,10 @@ Error: %2$@ Obrázek bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Ihned @@ -4272,6 +4297,10 @@ More improvements are coming soon! Pozvat přátele No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Pozvat členy @@ -4479,6 +4508,10 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + Links + No comment provided by engineer. + List swipe action @@ -4594,6 +4627,10 @@ This is your link for group %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports chat feature @@ -4614,12 +4651,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Člen bude odstraněn ze skupiny - toto nelze vzít zpět! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6074,7 +6111,11 @@ swipe action Remove Odstranit - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6096,7 +6137,7 @@ swipe action Remove member? Odebrat člena? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6471,10 +6512,30 @@ chat item action Search bar accepts invitation links. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary No comment provided by engineer. @@ -8156,6 +8217,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Video obdržíte, až bude váš kontakt online, vyčkejte prosím nebo zkontrolujte později! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Videa a soubory až do velikosti 1 gb diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 34bbab2a6a..cec79a1739 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -792,6 +792,10 @@ swipe action Alle Gruppenmitglieder bleiben verbunden. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Alle Nachrichten und Dateien werden **Ende-zu-Ende verschlüsselt** versendet - in Direkt-Nachrichten mit Post-Quantum-Security. @@ -1142,6 +1146,10 @@ swipe action Audio- und Videoanrufe No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Audio-/Video-Anrufe @@ -2579,6 +2587,14 @@ swipe action Nachricht des Mitglieds löschen? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Die Nachricht löschen? @@ -2587,7 +2603,8 @@ swipe action Delete messages Nachrichten löschen - alert button + alert action +alert button Delete messages after @@ -3851,6 +3868,10 @@ snd error text Dateien und Medien sind nicht erlaubt! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Nach ungelesenen und favorisierten Chats filtern. @@ -4335,6 +4356,10 @@ Fehler: %2$@ Das Bild wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Sofort @@ -4594,6 +4619,10 @@ Weitere Verbesserungen sind bald verfügbar! Freunde einladen No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Mitglieder einladen @@ -4817,6 +4846,10 @@ Das ist Ihr Link für die Gruppe %@! Verknüpfte Desktops No comment provided by engineer. + + Links + No comment provided by engineer. + List Liste @@ -4942,6 +4975,10 @@ Das ist Ihr Link für die Gruppe %@! Mitglied ist gelöscht - Anfrage kann nicht angenommen werden No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports Mitglieder-Meldungen @@ -4965,12 +5002,12 @@ Das ist Ihr Link für die Gruppe %@! Member will be removed from chat - this cannot be undone! Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6595,7 +6632,11 @@ swipe action Remove Entfernen - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6620,7 +6661,7 @@ swipe action Remove member? Das Mitglied entfernen? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -7038,11 +7079,31 @@ chat item action In der Suchleiste werden nun auch Einladungslinks angenommen. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Suchen oder SimpleX-Link einfügen No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Zweite Farbe @@ -8918,6 +8979,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Das Video wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Videos und Dateien bis zu 1GB diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 2f5a0acbb1..581cd791a5 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -792,6 +792,11 @@ swipe action All group members will remain connected. No comment provided by engineer. + + All messages + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. @@ -1142,6 +1147,11 @@ swipe action Audio and video calls No comment provided by engineer. + + Audio call + Audio call + No comment provided by engineer. + Audio/video calls Audio/video calls @@ -2579,6 +2589,16 @@ swipe action Delete member message? No comment provided by engineer. + + Delete member messages + Delete member messages + No comment provided by engineer. + + + Delete member messages? + Delete member messages? + alert title + Delete message? Delete message? @@ -2587,7 +2607,8 @@ swipe action Delete messages Delete messages - alert button + alert action +alert button Delete messages after @@ -3851,6 +3872,11 @@ snd error text Files and media prohibited! No comment provided by engineer. + + Filter + Filter + No comment provided by engineer. + Filter unread and favorite chats. Filter unread and favorite chats. @@ -4335,6 +4361,11 @@ Error: %2$@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. + + Images + Images + No comment provided by engineer. + Immediately Immediately @@ -4594,6 +4625,11 @@ More improvements are coming soon! Invite friends No comment provided by engineer. + + Invite member + Invite member + No comment provided by engineer. + Invite members Invite members @@ -4817,6 +4853,11 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + Links + Links + No comment provided by engineer. + List List @@ -4942,6 +4983,11 @@ This is your link for group %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + Member messages will be deleted - this cannot be undone! + alert message + Member reports Member reports @@ -4965,12 +5011,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! Member will be removed from chat - this cannot be undone! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Member will be removed from group - this cannot be undone! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6595,7 +6641,12 @@ swipe action Remove Remove - No comment provided by engineer. + alert action + + + Remove and delete messages + Remove and delete messages + alert action Remove archive? @@ -6620,7 +6671,7 @@ swipe action Remove member? Remove member? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -7038,11 +7089,36 @@ chat item action Search bar accepts invitation links. No comment provided by engineer. + + Search files + Search files + No comment provided by engineer. + + + Search images + Search images + No comment provided by engineer. + + + Search links + Search links + No comment provided by engineer. + Search or paste SimpleX link Search or paste SimpleX link No comment provided by engineer. + + Search videos + Search videos + No comment provided by engineer. + + + Search voice messages + Search voice messages + No comment provided by engineer. + Secondary Secondary @@ -8918,6 +8994,11 @@ To connect, please ask your contact to create another connection link and check Video will be received when your contact is online, please wait or check later! No comment provided by engineer. + + Videos + Videos + No comment provided by engineer. + Videos and files up to 1gb Videos and files up to 1gb diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 30c69af755..edacbd8e56 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -792,6 +792,10 @@ swipe action Todos los miembros del grupo permanecerán conectados. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Todos los mensajes y archivos son enviados **cifrados de extremo a extremo** y con seguridad de cifrado postcuántico en mensajes directos. @@ -1142,6 +1146,10 @@ swipe action Llamadas y videollamadas No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Llamadas y videollamadas @@ -2579,6 +2587,14 @@ swipe action ¿Eliminar el mensaje de miembro? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? ¿Eliminar mensaje? @@ -2587,7 +2603,8 @@ swipe action Delete messages Activar - alert button + alert action +alert button Delete messages after @@ -3851,6 +3868,10 @@ snd error text ¡Archivos y multimedia no permitidos! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Filtra chats no leídos y favoritos. @@ -4335,6 +4356,10 @@ Error: %2$@ La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Inmediatamente @@ -4594,6 +4619,10 @@ More improvements are coming soon! Invitar amigos No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Invitar miembros @@ -4817,6 +4846,10 @@ This is your link for group %@! Ordenadores enlazados No comment provided by engineer. + + Links + No comment provided by engineer. + List Lista @@ -4942,6 +4975,10 @@ This is your link for group %@! Miembro eliminado, no puede aceptar solicitudes No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports Informes de miembros @@ -4965,12 +5002,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! El miembro será eliminado del chat. ¡No puede deshacerse! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! El miembro será expulsado del grupo. ¡No puede deshacerse! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -5484,7 +5521,7 @@ This is your link for group %@! No direct connection yet, message is forwarded by admin. - Aún no hay conexión directa con este miembro, el mensaje es reenviado por el administrador. + Aún no hay conexión directa, los mensajes son reenviados por el administrador. item status description @@ -6595,7 +6632,11 @@ swipe action Remove Eliminar - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6620,7 +6661,7 @@ swipe action Remove member? ¿Expulsar miembro? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -7038,11 +7079,31 @@ chat item action La barra de búsqueda acepta enlaces de invitación. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Buscar o pegar enlace SimpleX No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Secundario @@ -8918,6 +8979,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Vídeos y archivos de hasta 1Gb @@ -9649,7 +9714,7 @@ Repeat connection request? accepted you - te ha aceptado + te ha admitido rcv group event chat item @@ -10623,7 +10688,7 @@ last received msg: %2$@ you accepted this member - has aceptado al miembro + has admitido al miembro snd group event chat item diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 56fa4a1485..00b4bca1d4 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -726,6 +726,10 @@ swipe action Kaikki ryhmän jäsenet pysyvät yhteydessä. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. No comment provided by engineer. @@ -1042,6 +1046,10 @@ swipe action Ääni- ja videopuhelut No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Ääni/videopuhelut @@ -2328,6 +2336,14 @@ swipe action Poista jäsenviesti? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Poista viesti? @@ -2336,7 +2352,8 @@ swipe action Delete messages Poista viestit - alert button + alert action +alert button Delete messages after @@ -3483,6 +3500,10 @@ snd error text Tiedostot ja media kielletty! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Suodata lukemattomia- ja suosikkikeskusteluja. @@ -3924,6 +3945,10 @@ Error: %2$@ Kuva vastaanotetaan, kun kontaktisi on verkossa, odota tai tarkista myöhemmin! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Heti @@ -4159,6 +4184,10 @@ More improvements are coming soon! Kutsu ystäviä No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Kutsu jäseniä @@ -4366,6 +4395,10 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + Links + No comment provided by engineer. + List swipe action @@ -4481,6 +4514,10 @@ This is your link for group %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports chat feature @@ -4501,12 +4538,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Jäsen poistetaan ryhmästä - tätä ei voi perua! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -5959,7 +5996,11 @@ swipe action Remove Poista - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -5981,7 +6022,7 @@ swipe action Remove member? Poista jäsen? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6356,10 +6397,30 @@ chat item action Search bar accepts invitation links. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary No comment provided by engineer. @@ -8038,6 +8099,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Video vastaanotetaan, kun kontaktisi on online-tilassa, odota tai tarkista myöhemmin! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Videot ja tiedostot 1 Gt asti diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 67485353d2..82912c5d44 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -792,6 +792,10 @@ swipe action Tous les membres du groupe resteront connectés. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Tous les messages et fichiers sont envoyés **chiffrés de bout en bout**, avec une sécurité post-quantique dans les messages directs. @@ -1141,6 +1145,10 @@ swipe action Appels audio et vidéo No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Appels audio/vidéo @@ -2564,6 +2572,14 @@ swipe action Supprimer le message de ce membre ? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Supprimer le message ? @@ -2572,7 +2588,8 @@ swipe action Delete messages Supprimer les messages - alert button + alert action +alert button Delete messages after @@ -3822,6 +3839,10 @@ snd error text Fichiers et médias interdits ! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Filtrer les messages non lus et favoris. @@ -4296,6 +4317,10 @@ Erreur : %2$@ L'image sera reçue quand votre contact sera en ligne, merci d'attendre ou de revenir plus tard ! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Immédiatement @@ -4548,6 +4573,10 @@ D'autres améliorations sont à venir ! Inviter des amis No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Inviter des membres @@ -4769,6 +4798,10 @@ Voici votre lien pour le groupe %@ ! Bureaux liés No comment provided by engineer. + + Links + No comment provided by engineer. + List swipe action @@ -4887,6 +4920,10 @@ Voici votre lien pour le groupe %@ ! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports chat feature @@ -4909,12 +4946,12 @@ Voici votre lien pour le groupe %@ ! Member will be removed from chat - this cannot be undone! Le membre sera retiré de la discussion - cela ne peut pas être annulé ! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Ce membre sera retiré du groupe - impossible de revenir en arrière ! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6489,7 +6526,11 @@ swipe action Remove Supprimer - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6513,7 +6554,7 @@ swipe action Remove member? Retirer ce membre ? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6912,11 +6953,31 @@ chat item action La barre de recherche accepte les liens d'invitation. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Rechercher ou coller un lien SimpleX No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Secondaire @@ -8743,6 +8804,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard ! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Vidéos et fichiers jusqu'à 1Go diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 22d223ffd9..99219c1f40 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -92,12 +92,12 @@ %@ is not verified - %@ nincs hitelesítve + %@ nincs ellenőrizve No comment provided by engineer. %@ is verified - %@ hitelesítve + %@ ellenőrizve No comment provided by engineer. @@ -217,7 +217,7 @@ %lld contact(s) selected - %lld partner kijelölve + %lld partner kiválasztva No comment provided by engineer. @@ -367,7 +367,7 @@ **Warning**: Instant push notifications require passphrase saved in Keychain. - **Figyelmeztetés:** Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges. + **Figyelmeztetés:** Az azonnali leküldéses értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges. No comment provided by engineer. @@ -377,12 +377,12 @@ **e2e encrypted** audio call - **e2e titkosított** hanghívás + **végpontok között titkosított** hanghívás No comment provided by engineer. **e2e encrypted** video call - **e2e titkosított** videóhívás + **végpontok között titkosított** videóhívás No comment provided by engineer. @@ -789,7 +789,11 @@ swipe action All group members will remain connected. - Az összes csoporttag kapcsolatban marad. + Az összes csoporttag továbbra is kapcsolatban marad. + No comment provided by engineer. + + + All messages No comment provided by engineer. @@ -829,12 +833,12 @@ swipe action All your contacts will remain connected. - Az összes partnerével kapcsolatban marad. + Az összes partnerével továbbra is kapcsolatban marad. No comment provided by engineer. All your contacts will remain connected. Profile update will be sent to your contacts. - A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. + Az összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. No comment provided by engineer. @@ -954,7 +958,7 @@ swipe action Allow your contacts to send disappearing messages. - Az eltűnő üzenetek küldésének engedélyezése a partnerei számára. + Az eltűnő üzenetek küldése engedélyezve van a partnerei számára. No comment provided by engineer. @@ -984,12 +988,12 @@ swipe action Always use private routing. - Mindig használjon privát útválasztást. + Mindig legyen használva privát útválasztás. No comment provided by engineer. Always use relay - Mindig használjon továbbítókiszolgálót + Mindig legyen használva továbbítókiszolgáló No comment provided by engineer. @@ -1142,6 +1146,10 @@ swipe action Hang- és videóhívások No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Hang- és videóhívások @@ -1264,12 +1272,12 @@ swipe action Bio - Névjegy + Életrajz No comment provided by engineer. Bio too large - A névjegy túl hosszú + Az életrajz túl hosszú alert title @@ -1398,7 +1406,7 @@ swipe action Call already ended! - A hívás már befejeződött! + A hívás már véget ért! No comment provided by engineer. @@ -1691,7 +1699,7 @@ set passcode view Choose _Migrate from another device_ on the new device and scan QR code. - Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközén és olvassa be a QR-kódot. + Válassza az _Átköltöztetés egy másik eszközről_ beállítást az új eszközén és olvassa be a QR-kódot. No comment provided by engineer. @@ -1721,37 +1729,37 @@ set passcode view Clear - Kiürítés + Ürítés swipe action Clear conversation - Üzenetek kiürítése + Üzenetek ürítése No comment provided by engineer. Clear conversation? - Kiüríti az üzeneteket? + Üríti a beszélgetés üzeneteit? No comment provided by engineer. Clear group? - Kiüríti a csoportot? + Üríti a csoport üzeneteit? No comment provided by engineer. Clear or delete group? - Csoport kiürítése vagy törlése? + Csoport ürítése vagy törlése? No comment provided by engineer. Clear private notes? - Kiüríti a privát jegyzeteket? + Üríti a privát jegyzetek tartalmát? No comment provided by engineer. Clear verification - Hitelesítés törlése + Ellenőrzés törlése No comment provided by engineer. @@ -1935,7 +1943,7 @@ Ez a saját egyszer használható meghívója! Connect via one-time link - Kapcsolódás egyszer használható meghívón keresztül + Kapcsolódás az egyszer használható meghívón keresztül new chat sheet title @@ -1960,7 +1968,7 @@ Ez a saját egyszer használható meghívója! Connected to desktop - Kapcsolódva a számítógéphez + Társítva a számítógéppel No comment provided by engineer. @@ -1985,7 +1993,7 @@ Ez a saját egyszer használható meghívója! Connecting to desktop - Kapcsolódás a számítógéphez + Társítás számítógéppel No comment provided by engineer. @@ -2212,7 +2220,7 @@ Ez a saját egyszer használható meghívója! Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 - Új profil létrehozása a [számítógép-alkalmazásban](https://simplex.chat/downloads/). 💻 + Új profil létrehozása a [számítógépes alkalmazásban](https://simplex.chat/downloads/). 💻 No comment provided by engineer. @@ -2456,7 +2464,7 @@ swipe action Delete all files - Az összes fájl törlése + Összes fájl törlése No comment provided by engineer. @@ -2579,6 +2587,14 @@ swipe action Törli a tag üzenetét? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Törli az üzenetet? @@ -2587,7 +2603,8 @@ swipe action Delete messages Üzenetek törlése - alert button + alert action +alert button Delete messages after @@ -2706,7 +2723,7 @@ swipe action Desktop app version %@ is not compatible with this app. - A számítógép-alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással. + A számítógépes alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással. No comment provided by engineer. @@ -2881,7 +2898,7 @@ swipe action Do NOT use private routing. - NE használjon privát útválasztást. + NE legyen használva privát útválasztás. No comment provided by engineer. @@ -2911,7 +2928,7 @@ swipe action Don't enable - Ne engedélyezze + Nem engedélyezem No comment provided by engineer. @@ -2921,7 +2938,7 @@ swipe action Don't show again - Ne mutasd újra + Ne jelenjen meg újra alert action @@ -2992,7 +3009,7 @@ chat item action E2E encrypted notifications. - Végpontok közötti titkosított értesítések. + Végpontok között titkosított értesítések. No comment provided by engineer. @@ -3102,7 +3119,7 @@ chat item action Encrypt - Titkosít + Titkosítás No comment provided by engineer. @@ -3632,7 +3649,7 @@ chat item action Error verifying passphrase: - Hiba történt a jelmondat hitelesítésekor: + Hiba történt a jelmondat ellenőrzésekor: No comment provided by engineer. @@ -3851,6 +3868,10 @@ snd error text A fájlok és a médiatartalmak küldése le van tiltva! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Olvasatlan és kedvenc csevegésekre való szűrés. @@ -4272,7 +4293,7 @@ Hiba: %2$@ How to - Hogyan + Útmutató No comment provided by engineer. @@ -4317,7 +4338,7 @@ Hiba: %2$@ If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). - Ha most kell használnia a csevegést, koppintson alább a **Befejezés később** lehetőségre (az alkalmazás újraindításakor fel lesz ajánlva az adatbázis átköltöztetése). + Ha most kell használnia a csevegést, koppintson lentebb a **Befejezés később** beállításra (az alkalmazás újraindításakor fel lesz ajánlva az adatbázis átköltöztetése). No comment provided by engineer. @@ -4335,6 +4356,10 @@ Hiba: %2$@ A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Azonnal @@ -4500,7 +4525,7 @@ További fejlesztések hamarosan! Instant push notifications will be hidden! - Az azonnali push-értesítések el lesznek rejtve! + Az azonnali leküldéses értesítések el lesznek rejtve! No comment provided by engineer. @@ -4594,6 +4619,10 @@ További fejlesztések hamarosan! Barátok meghívása No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Tagok meghívása @@ -4714,7 +4743,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Keep the app open to use it from desktop - A számítógépről való használathoz tartsd nyitva az alkalmazást + Alkalmazás megnyitva tartása a számítógépről való használathoz No comment provided by engineer. @@ -4804,7 +4833,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Link mobile and desktop apps! 🔗 - Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗 + Társítsa össze a hordozható eszköz- és a számítógépes alkalmazásokat! 🔗 No comment provided by engineer. @@ -4817,6 +4846,10 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Társított számítógépek No comment provided by engineer. + + Links + No comment provided by engineer. + List Lista @@ -4894,7 +4927,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Mark verified - Hitelesítés + Megjelölés ellenőrzöttként No comment provided by engineer. @@ -4904,7 +4937,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Max 30 seconds, received instantly. - Max. 30 másodperc, azonnal érkezett. + Legfeljebb 30 másodperc, azonnal megérkezik. No comment provided by engineer. @@ -4942,6 +4975,10 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! A tag törölve lett – nem lehet elfogadni a kérést No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports Tagok jelentései @@ -4965,12 +5002,12 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Member will be removed from chat - this cannot be undone! A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -5044,7 +5081,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Message draft - Üzenetvázlat + Piszkozatok No comment provided by engineer. @@ -5159,7 +5196,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Messages were deleted after you selected them. - Az üzeneteket törölték miután kijelölte őket. + Az üzeneteket törölték miután kiváasztotta őket. alert message @@ -5219,7 +5256,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). - Sikertelen átköltöztetés. Koppintson a **Kihagyás** lehetőségre a jelenlegi adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat). + Sikertelen átköltöztetés. Koppintson a **Kihagyás** beállításra a jelenlegi adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat). No comment provided by engineer. @@ -5374,12 +5411,12 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! New contact: - Új kapcsolat: + Új partner: notification New desktop app! - Új számítógép-alkalmazás! + Új számítógépes alkalmazás! No comment provided by engineer. @@ -5464,7 +5501,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! No contacts selected - Nincs partner kijelölve + Nincs partner kiválasztva No comment provided by engineer. @@ -5549,7 +5586,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! No push server - Helyi + Nincs kiszolgáló a leküldéses értesítésekhez No comment provided by engineer. @@ -5604,7 +5641,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Nothing selected - Nincs semmi kijelölve + Nincs semmi kiválasztva No comment provided by engineer. @@ -5739,17 +5776,17 @@ VPN engedélyezése szükséges. Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours) - Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra) + Csak Ön törölheti véglegesen az üzeneteket (partnere csak törlésre jelölheti meg azokat ). (24 óra) No comment provided by engineer. Only you can make calls. - Csak Ön tud hívásokat indítani. + Csak Ön kezdeményezhet hívásokat. No comment provided by engineer. Only you can send disappearing messages. - Csak Ön tud eltűnő üzeneteket küldeni. + Csak Ön küldhet eltűnő üzeneteket. No comment provided by engineer. @@ -5759,7 +5796,7 @@ VPN engedélyezése szükséges. Only you can send voice messages. - Csak Ön tud hangüzeneteket küldeni. + Csak Ön küldhet hangüzeneteket. No comment provided by engineer. @@ -5769,17 +5806,17 @@ VPN engedélyezése szükséges. Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours) - Csak a partnere tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra) + Csak a partnere törölheti véglegesen az üzeneteket (Ön csak törlésre jelölheti meg azokat). (24 óra) No comment provided by engineer. Only your contact can make calls. - Csak a partnere tud hívást indítani. + Csak a partnere kezdeményezhet hívásokat. No comment provided by engineer. Only your contact can send disappearing messages. - Csak a partnere tud eltűnő üzeneteket küldeni. + Csak a partnere küldhet eltűnő üzeneteket. No comment provided by engineer. @@ -5789,7 +5826,7 @@ VPN engedélyezése szükséges. Only your contact can send voice messages. - Csak a partnere tud hangüzeneteket küldeni. + Csak a partnere küldhet hangüzeneteket. No comment provided by engineer. @@ -6100,7 +6137,7 @@ Hiba: %@ Please restart the app and migrate the database to enable push notifications. - Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges push-értesítések engedélyezéséhez. + Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges leküldéses értesítések engedélyezéséhez. No comment provided by engineer. @@ -6285,7 +6322,7 @@ Hiba: %@ Prohibit reporting messages to moderators. - Az üzenetek a moderátorok felé történő jelentésének megtiltása. + Az üzenetek jelentése a moderátorok felé le van tiltva. No comment provided by engineer. @@ -6367,12 +6404,12 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Push notifications - Push-értesítések + Leküldéses értesítések No comment provided by engineer. Push server - Push-kiszolgáló + Leküldéses értesítéskiszolgáló No comment provided by engineer. @@ -6382,7 +6419,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Rate the app - Értékelje az alkalmazást + Alkalmazás értékelése No comment provided by engineer. @@ -6392,7 +6429,7 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. React… - Reagálj… + Reagálás… chat item menu @@ -6569,7 +6606,7 @@ swipe action Reject (sender NOT notified) - Elutasítás (a kérés küldője NEM fog értesítést kapni) + Elutasítás (a kérés küldője NEM lesz értesítve) No comment provided by engineer. @@ -6595,7 +6632,11 @@ swipe action Remove Eltávolítás - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6620,7 +6661,7 @@ swipe action Remove member? Eltávolítja a tagot? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6739,7 +6780,7 @@ swipe action Reset all statistics - Az összes statisztika visszaállítása + Összes statisztika visszaállítása No comment provided by engineer. @@ -7038,11 +7079,31 @@ chat item action A keresősáv elfogadja a meghívási hivatkozásokat. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Keresés vagy SimpleX-hivatkozás beillesztése No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Másodlagos szín @@ -7060,7 +7121,7 @@ chat item action Security assessment - Biztonsági kiértékelés + Biztonsági felmérés No comment provided by engineer. @@ -7070,22 +7131,22 @@ chat item action Select - Kijelölés + Kiválasztás chat item action Select chat profile - Csevegési profil kijelölése + Csevegési profil kiválasztása No comment provided by engineer. Selected %lld - %lld kijelölve + %lld kiválasztva No comment provided by engineer. Selected chat preferences prohibit this message. - A kijelölt csevegési beállítások tiltják ezt az üzenetet. + A kiválasztott csevegési beállítások tiltják ezt az üzenetet. No comment provided by engineer. @@ -7240,7 +7301,7 @@ chat item action Sending receipts is disabled for %lld contacts - A kézbesítési jelentések le vannak tiltva %lld partnernél + A kézbesítési jelentések le vannak tiltva %lld partner számára No comment provided by engineer. @@ -7250,7 +7311,7 @@ chat item action Sending receipts is enabled for %lld contacts - A kézbesítési jelentések engedélyezve vannak %lld partnernél + A kézbesítési jelentések engedélyezve vannak %lld partner számára No comment provided by engineer. @@ -7395,7 +7456,7 @@ chat item action Session code - Munkamenet kód + Munkamenet kódja No comment provided by engineer. @@ -7455,7 +7516,7 @@ chat item action Set profile bio and welcome message. - Névjegy és üdvözlőüzenet beállítása a profilokhoz. + Életrajz és üdvözlőüzenet beállítása a profilokhoz. No comment provided by engineer. @@ -7681,7 +7742,7 @@ chat item action SimpleX address settings - Beállítások automatikus elfogadása + SimpleX-címbeállítások alert title @@ -7756,7 +7817,7 @@ chat item action Small groups (max 20) - Kis csoportok (max. 20 tag) + Kis csoportok (legfeljebb 20 tag) No comment provided by engineer. @@ -7809,7 +7870,7 @@ report reason Start chat - Csevegés indítása + Csevegés elindítása No comment provided by engineer. @@ -8206,12 +8267,12 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. The second tick we missed! ✅ - A második jelölés, amit kihagytunk! ✅ + A második pipa, ami már nagyon hiányzott! ✅ No comment provided by engineer. The sender will NOT be notified - A kérés küldője NEM fog értesítést kapni + A kérés küldője NEM lesz értesítve alert message @@ -8261,12 +8322,12 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - Ez a művelet nem vonható vissza – a kijelöltnél korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet. + Ez a művelet nem vonható vissza – a kiválasztott üzenettől korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet. No comment provided by engineer. This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. - Ez a művelet nem vonható vissza – a kijelölt üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből. + Ez a művelet nem vonható vissza – a kiválasztott üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből. alert message @@ -8423,7 +8484,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To support instant push notifications the chat database has to be migrated. - Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges. + Az azonnali leküldéses értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges. No comment provided by engineer. @@ -8438,7 +8499,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To verify end-to-end encryption with your contact compare (or scan) the code on your devices. - A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal. + A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal. No comment provided by engineer. @@ -8640,7 +8701,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Update database passphrase - Az adatbázis jelmondatának módosítása + Adatbázis jelmondatának módosítása No comment provided by engineer. @@ -8845,7 +8906,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso User selection - Felhasználó kijelölése + Felhasználó kiválasztása No comment provided by engineer. @@ -8860,37 +8921,37 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Verify code with desktop - Kód hitelesítése a számítógépen + Kód ellenőrzése a számítógépen No comment provided by engineer. Verify connection - Kapcsolat hitelesítése + Kapcsolat ellenőrzése No comment provided by engineer. Verify connection security - Biztonságos kapcsolat hitelesítése + Biztonságos kapcsolat ellenőrzése No comment provided by engineer. Verify connections - Kapcsolatok hitelesítése + Kapcsolatok ellenőrzése No comment provided by engineer. Verify database passphrase - Az adatbázis jelmondatának hitelesítése + Adatbázis jelmondatának ellenőrzése No comment provided by engineer. Verify passphrase - Jelmondat hitelesítése + Jelmondat ellenőrzése No comment provided by engineer. Verify security code - Biztonsági kód hitelesítése + Biztonsági kód ellenőrzése No comment provided by engineer. @@ -8918,6 +8979,10 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Videók és fájlok legfeljebb 1GB méretig @@ -9202,7 +9267,7 @@ Megismétli a csatlakozási kérést? You are not connected to the server used to receive messages from this connection (no subscription). - Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs előfizetés). + Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs feliratkozás). subscription status explanation @@ -9287,7 +9352,7 @@ Megismétli a csatlakozási kérést? You can start chat via app Settings / Database or by restarting the app - A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el + A csevegés elindítható az alkalmazás „Beállítások / Adatbázis” menüjében vagy az alkalmazás újraindításával No comment provided by engineer. @@ -9322,7 +9387,7 @@ Megismétli a csatlakozási kérést? You could not be verified; please try again. - Nem sikerült hitelesíteni; próbálja meg újra. + Nem sikerült ellenőrizni; próbálja meg újra. No comment provided by engineer. @@ -9434,12 +9499,12 @@ Megismétli a kapcsolódási kérést? You will stop receiving messages from this chat. Chat history will be preserved. - Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. + Nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. No comment provided by engineer. You will stop receiving messages from this group. Chat history will be preserved. - Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak. + Nem fog több üzenetet kapni ebből a csoportból, de a csevegés előzményei megmaradnak. No comment provided by engineer. @@ -9514,7 +9579,7 @@ Megismétli a kapcsolódási kérést? Your contact sent a file that is larger than currently supported maximum size (%@). - A partnere a jelenleg megengedett maximális méretű (%@) fájlnál nagyobbat küldött. + A partnere a jelenleg támogatott legnagyobb (%@) fájlméretnél nagyobbat küldött. No comment provided by engineer. @@ -9524,7 +9589,7 @@ Megismétli a kapcsolódási kérést? Your contacts will remain connected. - A partnerei továbbra is kapcsolódva maradnak. + A partnereivel továbbra is kapcsolatban marad. No comment provided by engineer. @@ -9704,7 +9769,7 @@ Megismétli a kapcsolódási kérést? audio call (not e2e encrypted) - hanghívás (nem e2e titkosított) + hanghívás (végpontok között NEM titkosított) No comment provided by engineer. @@ -9845,7 +9910,7 @@ marked deleted chat item preview text connecting call… - kapcsolódási hívás… + hívás kapcsolása… call status @@ -9880,12 +9945,12 @@ marked deleted chat item preview text contact has e2e encryption - a partner e2e titkosítással rendelkezik + a partner végpontok közötti titkosítással rendelkezik No comment provided by engineer. contact has no e2e encryption - a partner nem rendelkezik e2e titkosítással + a partner nem rendelkezik végpontok közötti titkosítással No comment provided by engineer. @@ -9981,7 +10046,7 @@ pref value e2e encrypted - e2e titkosított + végpontok között titkosított No comment provided by engineer. @@ -10041,12 +10106,12 @@ pref value ended - befejeződött + hívás vége No comment provided by engineer. ended call %@ - %@ hívása befejeződött + %@ hívása véget ért call status @@ -10091,12 +10156,12 @@ pref value iOS Keychain is used to securely store passphrase - it allows receiving push notifications. - Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a push-értesítések fogadását. + Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a leküldéses értesítések fogadását. No comment provided by engineer. iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. - Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi a push-értesítések fogadását. + Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi a leküldéses értesítések fogadását. No comment provided by engineer. @@ -10261,12 +10326,12 @@ pref value no e2e encryption - nincs e2e titkosítás + nincs végpontok közötti titkosítás No comment provided by engineer. no subscription - nincs előfizetés + nincs feliratkozás No comment provided by engineer. @@ -10354,12 +10419,12 @@ time to disappear received answer… - válasz fogadása… + válasz érkezett… No comment provided by engineer. received confirmation… - visszaigazolás fogadása… + visszaigazolás érkezett… No comment provided by engineer. @@ -10498,7 +10563,7 @@ utoljára fogadott üzenet: %2$@ starting… - indítás… + hívás indítása… No comment provided by engineer. @@ -10583,7 +10648,7 @@ utoljára fogadott üzenet: %2$@ video call (not e2e encrypted) - videóhívás (nem e2e titkosított) + videóhívás (végpontok között NEM titkosított) No comment provided by engineer. @@ -10838,12 +10903,12 @@ utoljára fogadott üzenet: %2$@ Comment - Hozzászólás + Megjegyzés No comment provided by engineer. Currently maximum supported file size is %@. - Jelenleg támogatott legnagyobb fájl méret: %@. + Jelenleg támogatott legnagyobb fájlméret: %@. No comment provided by engineer. @@ -10948,7 +11013,7 @@ utoljára fogadott üzenet: %2$@ Selected chat preferences prohibit this message. - A kijelölt csevegési beállítások tiltják ezt az üzenetet. + A kiválasztott csevegési beállítások tiltják ezt az üzenetet. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 5f057cd8bb..e2c826f334 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -792,6 +792,10 @@ swipe action Tutti i membri del gruppo resteranno connessi. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Tutti i messaggi e i file vengono inviati **crittografati end-to-end**, con sicurezza resistenti alla quantistica nei messaggi diretti. @@ -1142,6 +1146,10 @@ swipe action Chiamate audio e video No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Chiamate audio/video @@ -2579,6 +2587,14 @@ swipe action Eliminare il messaggio del membro? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Eliminare il messaggio? @@ -2587,7 +2603,8 @@ swipe action Delete messages Elimina messaggi - alert button + alert action +alert button Delete messages after @@ -3851,6 +3868,10 @@ snd error text File e contenuti multimediali vietati! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Filtra le chat non lette e preferite. @@ -4335,6 +4356,10 @@ Errore: %2$@ L'immagine verrà ricevuta quando il tuo contatto sarà in linea, aspetta o controlla più tardi! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Immediatamente @@ -4594,6 +4619,10 @@ Altri miglioramenti sono in arrivo! Invita amici No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Invita membri @@ -4817,6 +4846,10 @@ Questo è il tuo link per il gruppo %@! Desktop collegati No comment provided by engineer. + + Links + No comment provided by engineer. + List Elenco @@ -4942,6 +4975,10 @@ Questo è il tuo link per il gruppo %@! Il membro è eliminato - impossibile accettare la richiesta No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports Segnalazioni dei membri @@ -4965,12 +5002,12 @@ Questo è il tuo link per il gruppo %@! Member will be removed from chat - this cannot be undone! Il membro verrà rimosso dalla chat, non è reversibile! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Il membro verrà rimosso dal gruppo, non è reversibile! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -5854,7 +5891,7 @@ Richiede l'attivazione della VPN. Open new group - Apri un gruppo nuovo + Apri il nuovo gruppo new chat action @@ -6595,7 +6632,11 @@ swipe action Remove Rimuovi - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6620,7 +6661,7 @@ swipe action Remove member? Rimuovere il membro? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -7038,11 +7079,31 @@ chat item action La barra di ricerca accetta i link di invito. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Cerca o incolla un link SimpleX No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Secondario @@ -8918,6 +8979,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Il video verrà ricevuto quando il tuo contatto sarà in linea, attendi o controlla più tardi! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Video e file fino a 1 GB diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 9a42ab3f7e..efd47aa52d 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -625,6 +625,7 @@ swipe action Active connections + アクティブな接続 No comment provided by engineer. @@ -634,10 +635,12 @@ swipe action Add friends + 友達を追加 No comment provided by engineer. Add list + リストを追加 No comment provided by engineer. @@ -661,6 +664,7 @@ swipe action Add team members + チームメンバーを追加 No comment provided by engineer. @@ -670,6 +674,7 @@ swipe action Add to list + リストに追加 No comment provided by engineer. @@ -719,6 +724,7 @@ swipe action Address settings + アドレス設定 No comment provided by engineer. @@ -742,6 +748,7 @@ swipe action All + すべて No comment provided by engineer. @@ -772,12 +779,17 @@ swipe action グループ全員の接続が継続します。 No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. No comment provided by engineer. All messages will be deleted - this cannot be undone! + すべてのメッセージが削除されます。この操作は元に戻せません! No comment provided by engineer. @@ -829,6 +841,7 @@ swipe action Allow calls? + 通話を許可しますか? No comment provided by engineer. @@ -838,6 +851,7 @@ swipe action Allow downgrade + ダウングレードを許可する No comment provided by engineer. @@ -969,6 +983,7 @@ swipe action Another reason + 他の理由 report reason @@ -1046,6 +1061,7 @@ swipe action Archive + アーカイブ No comment provided by engineer. @@ -1079,6 +1095,7 @@ swipe action Archived contacts + アーカイブされた連絡先 No comment provided by engineer. @@ -1100,6 +1117,10 @@ swipe action 音声通話とビデオ通話 No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls 音声/ビデオ通話 @@ -1350,6 +1371,7 @@ swipe action Can't change profile + プロフィールを変更できません alert title @@ -1375,6 +1397,7 @@ new chat action Cancel migration + 移行を中止する No comment provided by engineer. @@ -1384,6 +1407,7 @@ new chat action Cannot forward message + メッセージを転送できません No comment provided by engineer. @@ -1460,6 +1484,7 @@ set passcode view Chat + チャット No comment provided by engineer. @@ -1514,6 +1539,7 @@ set passcode view Chat list + チャット一覧 No comment provided by engineer. @@ -1570,6 +1596,7 @@ set passcode view Check messages every 20 min. + 20分おきにメッセージを確認する。 No comment provided by engineer. @@ -2407,6 +2434,14 @@ swipe action メンバーのメッセージを削除しますか? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? メッセージを削除しますか? @@ -2415,7 +2450,8 @@ swipe action Delete messages メッセージを削除 - alert button + alert action +alert button Delete messages after @@ -3565,6 +3601,10 @@ snd error text ファイルとメディアは禁止されています! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. 未読とお気に入りをフィルターします。 @@ -4006,6 +4046,10 @@ Error: %2$@ 連絡先がオンラインになったら受信されます。しばらくお待ちください! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately 即座に @@ -4241,6 +4285,10 @@ More improvements are coming soon! 友人を招待する No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members メンバーを招待する @@ -4448,6 +4496,10 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + Links + No comment provided by engineer. + List swipe action @@ -4563,6 +4615,10 @@ This is your link for group %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports chat feature @@ -4583,12 +4639,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! メンバーをグループから除名する (※元に戻せません※)! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6045,7 +6101,11 @@ swipe action Remove 削除 - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6067,7 +6127,7 @@ swipe action Remove member? メンバーを除名しますか? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6442,10 +6502,30 @@ chat item action Search bar accepts invitation links. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary No comment provided by engineer. @@ -8117,6 +8197,10 @@ To connect, please ask your contact to create another connection link and check 動画は相手がオンラインになったら受信されます。しばらくお待ちください! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb 1GBまでのビデオとファイル diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 8e0cdee3ca..955607acfd 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -790,6 +790,10 @@ swipe action Alle groepsleden blijven verbonden. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Alle berichten en bestanden worden **end-to-end versleuteld** verzonden, met post-quantumbeveiliging in directe berichten. @@ -1138,6 +1142,10 @@ swipe action Audio en video oproepen No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Audio/video oproepen @@ -2565,6 +2573,14 @@ swipe action Bericht van lid verwijderen? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Verwijder bericht? @@ -2573,7 +2589,8 @@ swipe action Delete messages Verwijder berichten - alert button + alert action +alert button Delete messages after @@ -3825,6 +3842,10 @@ snd error text Bestanden en media niet toegestaan! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Filter ongelezen en favoriete chats. @@ -4305,6 +4326,10 @@ Fout: %2$@ De afbeelding wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Onmiddellijk @@ -4564,6 +4589,10 @@ Binnenkort meer verbeteringen! Nodig vrienden uit No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Nodig leden uit @@ -4785,6 +4814,10 @@ Dit is jouw link voor groep %@! Gelinkte desktops No comment provided by engineer. + + Links + No comment provided by engineer. + List Lijst @@ -4907,6 +4940,10 @@ Dit is jouw link voor groep %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports Ledenrapporten @@ -4930,12 +4967,12 @@ Dit is jouw link voor groep %@! Member will be removed from chat - this cannot be undone! Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6544,7 +6581,11 @@ swipe action Remove Verwijderen - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6568,7 +6609,7 @@ swipe action Remove member? Lid verwijderen? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6982,11 +7023,31 @@ chat item action Zoekbalk accepteert uitnodigingslinks. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Zoeken of plak een SimpleX link No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Secundair @@ -8832,6 +8893,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak De video wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Video's en bestanden tot 1 GB diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index fb46bcd1d9..f74e1e31b5 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -568,10 +568,12 @@ swipe action Accept as member + Zaakceptuj jako członka alert action Accept as observer + Zaakceptuj jako obserwatora alert action @@ -586,6 +588,7 @@ swipe action Accept contact request + Zaakceptuj prośby o kontakt alert title @@ -601,6 +604,7 @@ swipe action Accept member + Zaakceptuj członka alert title @@ -645,6 +649,7 @@ swipe action Add message + Dodaj wiadomość placeholder for sending contact request @@ -787,6 +792,10 @@ swipe action Wszyscy członkowie grupy pozostaną połączeni. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Wszystkie wiadomości i pliki są wysyłane **z szyfrowaniem end-to-end**, z bezpieczeństwem postkwantowym w wiadomościach bezpośrednich. @@ -819,6 +828,7 @@ swipe action All servers + Wszystkie serwery No comment provided by engineer. @@ -863,6 +873,7 @@ swipe action Allow files and media only if your contact allows them. + Zezwalaj na pliki i media tylko wtedy, gdy Twój kontakt na to pozwala. No comment provided by engineer. @@ -952,6 +963,7 @@ swipe action Allow your contacts to send files and media. + Pozwól kontaktom wysyłać pliki i media. No comment provided by engineer. @@ -1134,6 +1146,10 @@ swipe action Połączenia audio i wideo No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Połączenia audio/wideo @@ -1216,6 +1232,7 @@ swipe action Better groups performance + Lepsze działanie grup No comment provided by engineer. @@ -1240,6 +1257,7 @@ swipe action Better privacy and security + Lepsza prywatność i bezpieczeństwo No comment provided by engineer. @@ -1312,6 +1330,7 @@ swipe action Bot + Bot No comment provided by engineer. @@ -1336,6 +1355,7 @@ swipe action Both you and your contact can send files and media. + Zarówno Ty, jak i Twój kontakt możecie wysyłać pliki i media. No comment provided by engineer. @@ -2528,6 +2548,14 @@ swipe action Usunąć wiadomość członka? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Usunąć wiadomość? @@ -2536,7 +2564,8 @@ swipe action Delete messages Usuń wiadomości - alert button + alert action +alert button Delete messages after @@ -3755,6 +3784,10 @@ snd error text Pliki i media zabronione! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Filtruj nieprzeczytane i ulubione czaty. @@ -4222,6 +4255,10 @@ Błąd: %2$@ Obraz zostanie odebrany, gdy kontakt będzie online, poczekaj lub sprawdź później! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Natychmiast @@ -4472,6 +4509,10 @@ More improvements are coming soon! Zaproś znajomych No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Zaproś członków @@ -4690,6 +4731,10 @@ To jest twój link do grupy %@! Połączone komputery No comment provided by engineer. + + Links + No comment provided by engineer. + List swipe action @@ -4808,6 +4853,10 @@ To jest twój link do grupy %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports chat feature @@ -4828,12 +4877,12 @@ To jest twój link do grupy %@! Member will be removed from chat - this cannot be undone! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Członek zostanie usunięty z grupy - nie można tego cofnąć! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6387,7 +6436,11 @@ swipe action Remove Usuń - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6411,7 +6464,7 @@ swipe action Remove member? Usunąć członka? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6809,11 +6862,31 @@ chat item action Pasek wyszukiwania akceptuje linki zaproszenia. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Wyszukaj lub wklej link SimpleX No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Drugorzędny @@ -8609,6 +8682,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Film zostanie odebrany, gdy kontakt będzie online, poczekaj lub sprawdź później! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Filmy i pliki do 1gb diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index d885db1350..64905cf68c 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -167,7 +167,7 @@ %d hours - %d час. + %d ч. time interval @@ -792,6 +792,10 @@ swipe action Все члены группы останутся соединены. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах. @@ -1134,7 +1138,7 @@ swipe action Audio & video calls - Аудио- и видеозвонки + Аудио и видеозвонки No comment provided by engineer. @@ -1142,6 +1146,10 @@ swipe action Аудио и видео звонки No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Аудио/видео звонки @@ -2579,6 +2587,14 @@ swipe action Удалить сообщение участника? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Удалить сообщение? @@ -2587,7 +2603,8 @@ swipe action Delete messages Удалить сообщения - alert button + alert action +alert button Delete messages after @@ -3312,6 +3329,7 @@ chat item action Error connecting to the server used to receive messages from this connection: %@ + Ошибка подключения к серверу, используемому для получения сообщений от этого соединения: %@ subscription status explanation @@ -3850,6 +3868,10 @@ snd error text Файлы и медиа запрещены! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Фильтровать непрочитанные и избранные чаты. @@ -4334,6 +4356,10 @@ Error: %2$@ Изображение будет принято, когда Ваш контакт будет в сети, подождите или проверьте позже! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Сразу @@ -4592,6 +4618,10 @@ More improvements are coming soon! Пригласить друзей No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Пригласить членов группы @@ -4815,6 +4845,10 @@ This is your link for group %@! Связанные компьютеры No comment provided by engineer. + + Links + No comment provided by engineer. + List Список @@ -4940,6 +4974,10 @@ This is your link for group %@! Член группы удалён - невозможно принять запрос No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports Сообщения о нарушениях @@ -4963,12 +5001,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! Член будет удален из разговора - это действие нельзя отменить! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Член группы будет удален - это действие нельзя отменить! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6593,7 +6631,11 @@ swipe action Remove Удалить - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6618,7 +6660,7 @@ swipe action Remove member? Удалить члена группы? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -7036,11 +7078,31 @@ chat item action Поле поиска поддерживает ссылки-приглашения. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Искать или вставьте ссылку SimpleX No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Вторичный @@ -8411,7 +8473,7 @@ You will be prompted to complete authentication before this feature is enabled.< To send - Для оправки + Для отправки No comment provided by engineer. @@ -8476,6 +8538,7 @@ You will be prompted to complete authentication before this feature is enabled.< Trying to connect to the server used to receive messages from this connection. + Попытка подключиться к серверу, используемому для получения сообщений от этого соединения. subscription status explanation @@ -8915,6 +8978,10 @@ To connect, please ask your contact to create another connection link and check Видео будет получено, когда Ваш контакт будет онлайн, пожалуйста, подождите или проверьте позже! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Видео и файлы до 1гб @@ -9189,6 +9256,7 @@ Repeat join request? You are connected to the server used to receive messages from this connection. + Вы подключены к серверу, используемому для приема сообщений от этого соединения. subscription status explanation @@ -9198,6 +9266,7 @@ Repeat join request? You are not connected to the server used to receive messages from this connection (no subscription). + Вы не подключены к серверу, используемому для получения сообщений по этому соединению (нет подписки). subscription status explanation @@ -10261,6 +10330,7 @@ pref value no subscription + нет подписки No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index ecb4d20fbb..4ff953c62e 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -718,6 +718,10 @@ swipe action สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. No comment provided by engineer. @@ -1034,6 +1038,10 @@ swipe action การโทรด้วยเสียงและวิดีโอ No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls การโทรด้วยเสียง/วิดีโอ @@ -2317,6 +2325,14 @@ swipe action ลบข้อความสมาชิก? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? ลบข้อความ? @@ -2325,7 +2341,8 @@ swipe action Delete messages ลบข้อความ - alert button + alert action +alert button Delete messages after @@ -3468,6 +3485,10 @@ snd error text ไฟล์และสื่อต้องห้าม! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. กรองแชทที่ยังไม่อ่านและแชทโปรด @@ -3909,6 +3930,10 @@ Error: %2$@ จะได้รับรูปภาพเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately โดยทันที @@ -4142,6 +4167,10 @@ More improvements are coming soon! เชิญเพื่อนๆ No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members เชิญสมาชิก @@ -4349,6 +4378,10 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + Links + No comment provided by engineer. + List swipe action @@ -4464,6 +4497,10 @@ This is your link for group %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports chat feature @@ -4484,12 +4521,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -5936,7 +5973,11 @@ swipe action Remove ลบ - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -5958,7 +5999,7 @@ swipe action Remove member? ลบสมาชิกออก? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6333,10 +6374,30 @@ chat item action Search bar accepts invitation links. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary No comment provided by engineer. @@ -8008,6 +8069,10 @@ To connect, please ask your contact to create another connection link and check จะได้รับวิดีโอเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb วิดีโอและไฟล์สูงสุด 1gb diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 57151a95b5..346d9a2bdc 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -792,6 +792,10 @@ swipe action Tüm grup üyeleri bağlı kalacaktır. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Bütün mesajlar ve dosyalar **uçtan-uca şifrelemeli** gönderilir, doğrudan mesajlarda kuantum güvenlik ile birlikte. @@ -1142,6 +1146,10 @@ swipe action Sesli ve görüntülü aramalar No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Sesli/görüntülü aramalar @@ -2579,6 +2587,14 @@ swipe action Kişinin mesajı silinsin mi? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Mesaj silinsin mi? @@ -2587,7 +2603,8 @@ swipe action Delete messages Mesajları sil - alert button + alert action +alert button Delete messages after @@ -3849,6 +3866,10 @@ snd error text Dosyalar ve medya yasaklandı! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Favori ve okunmamış sohbetleri filtrele. @@ -4330,6 +4351,10 @@ Hata: %2$@ Kişi çevrimiçi olduğunda fotoğraf alınacaktır, lütfen bekleyin veya daha sonra kontrol et! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Hemen @@ -4589,6 +4614,10 @@ Daha fazla iyileştirme yakında geliyor! Arkadaşları davet et No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Üyeleri davet et @@ -4812,6 +4841,10 @@ Bu senin grup için bağlantın %@! Bağlanmış bilgisayarlar No comment provided by engineer. + + Links + No comment provided by engineer. + List Liste @@ -4937,6 +4970,10 @@ Bu senin grup için bağlantın %@! Üye silinmiş - istek kabul edilemez No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports Üye raporları @@ -4960,12 +4997,12 @@ Bu senin grup için bağlantın %@! Member will be removed from chat - this cannot be undone! Üye sohbetten kaldırılacak - bu geri alınamaz! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Üye gruptan çıkarılacaktır - bu geri alınamaz! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6590,7 +6627,11 @@ swipe action Remove Sil - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6615,7 +6656,7 @@ swipe action Remove member? Kişi silinsin mi? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -7033,11 +7074,31 @@ chat item action Arama çubuğu davet bağlantılarını kabul eder. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Ara veya SimpleX bağlantısını yapıştır No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary İkincil renk @@ -8912,6 +8973,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Kişiniz çevrimiçi olduğunda video alınacaktır, lütfen bekleyin veya daha sonra kontrol edin! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb 1gb'a kadar videolar ve dosyalar diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 7980685349..6c103e17e1 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -792,6 +792,10 @@ swipe action Всі учасники групи залишаться на зв'язку. No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. Всі повідомлення та файли надсилаються **наскрізним шифруванням**, з пост-квантовим захистом у прямих повідомленнях. @@ -1140,6 +1144,10 @@ swipe action Аудіо та відеодзвінки No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls Аудіо/відео дзвінки @@ -2574,6 +2582,14 @@ swipe action Видалити повідомлення учасника? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? Видалити повідомлення? @@ -2582,7 +2598,8 @@ swipe action Delete messages Видалити повідомлення - alert button + alert action +alert button Delete messages after @@ -3841,6 +3858,10 @@ snd error text Файли та медіа заборонені! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. Фільтруйте непрочитані та улюблені чати. @@ -4322,6 +4343,10 @@ Error: %2$@ Зображення буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately Негайно @@ -4581,6 +4606,10 @@ More improvements are coming soon! Запросити друзів No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members Запросити учасників @@ -4804,6 +4833,10 @@ This is your link for group %@! Пов'язані робочі столи No comment provided by engineer. + + Links + No comment provided by engineer. + List Список @@ -4927,6 +4960,10 @@ This is your link for group %@! Member is deleted - can't accept request No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports Повідомлення учасників @@ -4950,12 +4987,12 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! Учасника буде видалено з чату – це неможливо скасувати! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! Учасник буде видалений з групи - це неможливо скасувати! - No comment provided by engineer. + alert message Member will join the group, accept member? @@ -6575,7 +6612,11 @@ swipe action Remove Видалити - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? @@ -6599,7 +6640,7 @@ swipe action Remove member? Видалити учасника? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -7017,11 +7058,31 @@ chat item action Рядок пошуку приймає посилання-запрошення. No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link Знайдіть або вставте посилання SimpleX No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary Вторинний @@ -8892,6 +8953,10 @@ To connect, please ask your contact to create another connection link and check Відео буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb Відео та файли до 1 Гб diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index e1ce65b5ce..ff7b4fa141 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -568,10 +568,12 @@ swipe action Accept as member + 接受为成员 alert action Accept as observer + 接受为观察员 alert action @@ -586,6 +588,7 @@ swipe action Accept contact request + 接受联络请求 alert title @@ -601,6 +604,7 @@ swipe action Accept member + 接受成员 alert title @@ -645,6 +649,7 @@ swipe action Add message + 添加信息 placeholder for sending contact request @@ -787,6 +792,10 @@ swipe action 所有群组成员将保持连接。 No comment provided by engineer. + + All messages + No comment provided by engineer. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. 所有消息和文件均通过**端到端加密**发送;私信以量子安全方式发送。 @@ -864,6 +873,7 @@ swipe action Allow files and media only if your contact allows them. + 只有你的联系人允许的情况下才允许文件和媒体。 No comment provided by engineer. @@ -953,6 +963,7 @@ swipe action Allow your contacts to send files and media. + 允许你的联系人发送文件和媒体。 No comment provided by engineer. @@ -1135,6 +1146,10 @@ swipe action 语音和视频通话 No comment provided by engineer. + + Audio call + No comment provided by engineer. + Audio/video calls 音频/视频通话 @@ -1257,10 +1272,12 @@ swipe action Bio + 自我介绍 No comment provided by engineer. Bio too large + 自我介绍过大 alert title @@ -1315,6 +1332,7 @@ swipe action Bot + 机器人 No comment provided by engineer. @@ -1339,6 +1357,7 @@ swipe action Both you and your contact can send files and media. + 你和你的联系人都可发送文件和媒体。 No comment provided by engineer. @@ -1363,6 +1382,7 @@ swipe action Business connection + 企业连接 No comment provided by engineer. @@ -1416,6 +1436,7 @@ swipe action Can't change profile + 无法更改个人资料 alert title @@ -1633,14 +1654,17 @@ set passcode view Chat with admins + 和管理员聊天 chat toolbar Chat with member + 和成员聊天 No comment provided by engineer. Chat with members before they join. + 在成员加入前和这些人聊天 No comment provided by engineer. @@ -1650,6 +1674,7 @@ set passcode view Chats with members + 和成员聊天 No comment provided by engineer. @@ -1879,6 +1904,7 @@ set passcode view Connect faster! 🚀 + 更快地连接!🚀 No comment provided by engineer. @@ -2088,6 +2114,7 @@ This is your own one-time link! Contact requests from groups + 来自群的联络请求 No comment provided by engineer. @@ -2207,6 +2234,7 @@ This is your own one-time link! Create your address + 创建地址 No comment provided by engineer. @@ -2465,6 +2493,7 @@ swipe action Delete chat with member? + 删除和成员的聊天吗? alert title @@ -2557,6 +2586,14 @@ swipe action 删除成员消息? No comment provided by engineer. + + Delete member messages + No comment provided by engineer. + + + Delete member messages? + alert title + Delete message? 删除消息吗? @@ -2565,7 +2602,8 @@ swipe action Delete messages 删除消息 - alert button + alert action +alert button Delete messages after @@ -2664,6 +2702,7 @@ swipe action Deprecated options + 已废弃的选项 No comment provided by engineer. @@ -2673,6 +2712,7 @@ swipe action Description too large + 描述过大 alert title @@ -2983,6 +3023,7 @@ chat item action Empty message! + 空消息! No comment provided by engineer. @@ -3022,6 +3063,7 @@ chat item action Enable disappearing messages by default. + 默认启用定时消失消息。 No comment provided by engineer. @@ -3226,6 +3268,7 @@ chat item action Error accepting member + 接受成员出错 alert title @@ -3240,6 +3283,7 @@ chat item action Error adding short link + 添加短链接出错 No comment provided by engineer. @@ -3249,6 +3293,7 @@ chat item action Error changing chat profile + 更改聊天资料出错 alert title @@ -3273,6 +3318,7 @@ chat item action Error checking token status + 查询token状态出错 No comment provided by engineer. @@ -3331,6 +3377,7 @@ chat item action Error deleting chat + 删除聊天出错 alert title @@ -3425,6 +3472,7 @@ chat item action Error opening group + 打开群时出错 No comment provided by engineer. @@ -3449,6 +3497,7 @@ chat item action Error rejecting contact request + 拒绝联络请求出错 alert title @@ -3528,6 +3577,7 @@ chat item action Error setting auto-accept + 设置自动接受出错 No comment provided by engineer. @@ -3614,6 +3664,7 @@ snd error text Error: %@. + 错误:%@。 server test error @@ -3797,6 +3848,7 @@ snd error text Files and media are prohibited in this chat. + 此聊天禁止文件和媒体。 No comment provided by engineer. @@ -3814,6 +3866,10 @@ snd error text 禁止文件和媒体! No comment provided by engineer. + + Filter + No comment provided by engineer. + Filter unread and favorite chats. 过滤未读和收藏的聊天记录。 @@ -3841,10 +3897,12 @@ snd error text Fingerprint in destination server address does not match certificate: %@. + 目的地服务器的指纹与证书不符:%@。 No comment provided by engineer. Fingerprint in forwarding server address does not match certificate: %@. + 转发服务器的指纹与证书不符:%@。 No comment provided by engineer. @@ -3854,6 +3912,7 @@ snd error text Fingerprint in server address does not match certificate: %@. + 服务器的指纹与证书不符:%@。 No comment provided by engineer. @@ -4132,6 +4191,7 @@ Error: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. + 群资料已修改。如果你进行保存,修改后的群资料将发送给其他群成员。 alert message @@ -4294,6 +4354,10 @@ Error: %2$@ 图片将在您的联系人在线时收到,请稍等或稍后查看! No comment provided by engineer. + + Images + No comment provided by engineer. + Immediately 立即 @@ -4553,6 +4617,10 @@ More improvements are coming soon! 邀请朋友 No comment provided by engineer. + + Invite member + No comment provided by engineer. + Invite members 邀请成员 @@ -4683,6 +4751,7 @@ This is your link for group %@! Keep your chats clean + 保持聊天洁净 No comment provided by engineer. @@ -4742,6 +4811,7 @@ This is your link for group %@! Less traffic on mobile networks. + 消耗更少的移动网络数据。 No comment provided by engineer. @@ -4774,6 +4844,10 @@ This is your link for group %@! 已链接桌面 No comment provided by engineer. + + Links + No comment provided by engineer. + List 列表 @@ -4801,6 +4875,7 @@ This is your link for group %@! Loading profile… + 正加载个人资料… in progress text @@ -4880,10 +4955,12 @@ This is your link for group %@! Member %@ + 成员 %@ past/unknown group member Member admission + 成员准入 No comment provided by engineer. @@ -4893,8 +4970,13 @@ This is your link for group %@! Member is deleted - can't accept request + 成员被删除——无法接受请求 No comment provided by engineer. + + Member messages will be deleted - this cannot be undone! + alert message + Member reports 成员举报 @@ -4918,15 +5000,16 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! 将从聊天中删除成员 - 此操作无法撤销! - No comment provided by engineer. + alert message Member will be removed from group - this cannot be undone! 成员将被移出群组——此操作无法撤消! - No comment provided by engineer. + alert message Member will join the group, accept member? + 成员将加入本群,接受成员吗? alert message @@ -5006,6 +5089,7 @@ This is your link for group %@! Message instantly once you tap Connect. + 轻按连接后即刻发消息。 No comment provided by engineer. @@ -5085,6 +5169,7 @@ This is your link for group %@! Messages are protected by **end-to-end encryption**. + 消息已通过**端到端加密**保护。 No comment provided by engineer. @@ -5344,6 +5429,7 @@ This is your link for group %@! New group role: Moderator + 新的群角色:协管 No comment provided by engineer. @@ -5363,6 +5449,7 @@ This is your link for group %@! New member wants to join the group. + 新成员要加入本群。 rcv group event chat item @@ -5407,6 +5494,7 @@ This is your link for group %@! No chats with members + 没有和成员的聊天 No comment provided by engineer. @@ -5491,6 +5579,7 @@ This is your link for group %@! No private routing session + 无私密路由会话 alert title @@ -5700,6 +5789,7 @@ Requires compatible VPN. Only you can send files and media. + 只有你可以发送文件和媒体。 No comment provided by engineer. @@ -5729,6 +5819,7 @@ Requires compatible VPN. Only your contact can send files and media. + 只有你的联系人可以发送文件和媒体。 No comment provided by engineer. @@ -5763,6 +5854,7 @@ Requires compatible VPN. Open clean link + 打开干净链接 alert action @@ -5772,6 +5864,7 @@ Requires compatible VPN. Open full link + 打开完整链接 alert action @@ -5781,6 +5874,7 @@ Requires compatible VPN. Open link? + 打开链接? alert title @@ -5790,26 +5884,32 @@ Requires compatible VPN. Open new chat + 打开新聊天 new chat action Open new group + 打开新群 new chat action Open to accept + 打开以接受 No comment provided by engineer. Open to connect + 打开以连接 No comment provided by engineer. Open to join + 打开以加入 No comment provided by engineer. Open to use bot + 打开来使用机器人 No comment provided by engineer. @@ -5870,6 +5970,8 @@ Requires compatible VPN. Other file errors: %@ + 其他文件错误: +%@ alert message @@ -6048,18 +6150,22 @@ Error: %@ Please try to disable and re-enable notfications. + 请尝试禁用并重新启用通知。 token info Please wait for group moderators to review your request to join the group. + 请等待群的协管审核你加入该群的请求。 snd group event chat item Please wait for token activation to complete. + 请等待token激活完成。 token info Please wait for token to be registered. + 请等待token注册完成。 token info @@ -6069,6 +6175,7 @@ Error: %@ Port + 端口 No comment provided by engineer. @@ -6083,6 +6190,7 @@ Error: %@ Preset servers + 预设服务器 No comment provided by engineer. @@ -6102,6 +6210,7 @@ Error: %@ Privacy for your customers. + 客户隐私。 No comment provided by engineer. @@ -6126,6 +6235,7 @@ Error: %@ Private media file names. + 私密媒体文件名。 No comment provided by engineer. @@ -6155,6 +6265,7 @@ Error: %@ Private routing timeout + 私密路由超时 alert title @@ -6209,6 +6320,7 @@ Error: %@ Prohibit reporting messages to moderators. + 禁止向 协管 举报消息。 No comment provided by engineer. @@ -6260,6 +6372,7 @@ Enable in *Network & servers* settings. Protocol background timeout + 协议后台超时 No comment provided by engineer. @@ -6284,6 +6397,7 @@ Enable in *Network & servers* settings. Proxy requires password + 代理需要密码 No comment provided by engineer. @@ -6468,6 +6582,7 @@ Enable in *Network & servers* settings. Register + 注册 No comment provided by engineer. @@ -6476,6 +6591,7 @@ Enable in *Network & servers* settings. Registered + 已注册 token status text @@ -6497,6 +6613,7 @@ swipe action Reject member? + 拒绝成员? alert title @@ -6512,10 +6629,15 @@ swipe action Remove 移除 - No comment provided by engineer. + alert action + + + Remove and delete messages + alert action Remove archive? + 删除存档? No comment provided by engineer. @@ -6525,6 +6647,7 @@ swipe action Remove link tracking + 删除链接跟踪 No comment provided by engineer. @@ -6535,7 +6658,7 @@ swipe action Remove member? 删除成员吗? - No comment provided by engineer. + alert title Remove passphrase from keychain? @@ -6544,6 +6667,7 @@ swipe action Removes messages and blocks members. + 删除消息并封禁成员。 No comment provided by engineer. @@ -6583,46 +6707,57 @@ swipe action Report + 举报 chat item action Report content: only group moderators will see it. + 举报内容:仅协管会看到。 report reason Report member profile: only group moderators will see it. + 举报成员个人资料:仅协管会看到。 report reason Report other: only group moderators will see it. + 举报其他:仅协管会看到。 report reason Report reason? + 举报理由? No comment provided by engineer. Report sent to moderators + 举报已发送至 协管 alert title Report spam: only group moderators will see it. + 举报垃圾信息:仅协管会看到。 report reason Report violation: only group moderators will see it. + 举报违规:仅协管会看到。 report reason Report: %@ + 举报: %@ report in notification Reporting messages to moderators is prohibited. + 向协管举报消息已被禁止。 No comment provided by engineer. Reports + 举报 No comment provided by engineer. @@ -6717,10 +6852,12 @@ swipe action Review group members + 审核群成员 No comment provided by engineer. Review members + 审核成员 admission stage @@ -6759,6 +6896,7 @@ swipe action SOCKS proxy + SOCKS代理 No comment provided by engineer. @@ -6784,10 +6922,12 @@ chat item action Save (and notify members) + 保存(并通知成员) alert button Save admission settings? + 保存入群设置? alert title @@ -6817,6 +6957,7 @@ chat item action Save group profile? + 保存群资料? alert title @@ -6934,11 +7075,31 @@ chat item action 搜索栏接受邀请链接。 No comment provided by engineer. + + Search files + No comment provided by engineer. + + + Search images + No comment provided by engineer. + + + Search links + No comment provided by engineer. + Search or paste SimpleX link 搜索或粘贴 SimpleX 链接 No comment provided by engineer. + + Search videos + No comment provided by engineer. + + + Search voice messages + No comment provided by engineer. + Secondary 二级 @@ -6971,6 +7132,7 @@ chat item action Select chat profile + 选择聊天个人资料 No comment provided by engineer. @@ -7015,6 +7177,7 @@ chat item action Send contact request? + 发送联络请求? No comment provided by engineer. @@ -7069,6 +7232,7 @@ chat item action Send private reports + 发送私下举报 No comment provided by engineer. @@ -7083,10 +7247,12 @@ chat item action Send request + 发送请求 No comment provided by engineer. Send request without message + 发送无消息请求 No comment provided by engineer. @@ -7101,6 +7267,7 @@ chat item action Send your private feedback to groups. + 向群发送私密反馈。 No comment provided by engineer. @@ -7200,10 +7367,12 @@ chat item action Server + 服务器 No comment provided by engineer. Server added to operator %@. + 服务器已添加到运营方 %@。 alert message @@ -7223,14 +7392,17 @@ chat item action Server operator changed. + 服务器运营方已更改。 alert title Server operators + 服务器运营方 No comment provided by engineer. Server protocol changed. + 服务器协议已更改。 alert title @@ -7290,6 +7462,7 @@ chat item action Set chat name… + 设置聊天名称… No comment provided by engineer. @@ -7314,10 +7487,12 @@ chat item action Set member admission + 设置成员入群准许 No comment provided by engineer. Set message expiration in chats. + 在聊天中设置消息过期时间。 No comment provided by engineer. @@ -7337,6 +7512,7 @@ chat item action Set profile bio and welcome message. + 设置自我介绍和欢迎消息。 No comment provided by engineer. @@ -7356,6 +7532,7 @@ chat item action Settings were changed. + 设置已修改。 alert message @@ -7376,10 +7553,12 @@ chat item action Share 1-time link with a friend + 和一位好友分享一次性链接 No comment provided by engineer. Share SimpleX address on social media. + 在社媒上分享 SimpleX 地址。 No comment provided by engineer. @@ -7389,6 +7568,7 @@ chat item action Share address publicly + 公开分享地址 No comment provided by engineer. @@ -7408,14 +7588,17 @@ chat item action Share old address + 分享旧地址 alert button Share old link + 分享旧链接 alert button Share profile + 分享资料 No comment provided by engineer. @@ -7435,18 +7618,22 @@ chat item action Share your address + 分享地址 No comment provided by engineer. Short SimpleX address + SimpleX 短地址 No comment provided by engineer. Short description + 短描述 No comment provided by engineer. Short link + 短链接 No comment provided by engineer. @@ -7601,6 +7788,7 @@ chat item action SimpleX relay link + SimpleX 中继链接 simplex link type @@ -7656,6 +7844,8 @@ chat item action Some servers failed the test: %@ + 有服务器测试未通过: +%@ alert message @@ -7665,6 +7855,7 @@ chat item action Spam + 垃圾信息 blocking reason report reason @@ -7789,10 +7980,12 @@ report reason Switch audio and video during the call. + 通话期间切换音频和视频。 No comment provided by engineer. Switch chat profile for 1-time invitations. + 对一次性邀请切换聊天个人资料。 No comment provided by engineer. @@ -7812,6 +8005,7 @@ report reason TCP connection bg timeout + TCP 连接后台超时 No comment provided by engineer. @@ -7821,6 +8015,7 @@ report reason TCP port for messaging + 用于消息收发的 TCP 端口 No comment provided by engineer. @@ -7840,6 +8035,7 @@ report reason Tail + 尾部 No comment provided by engineer. @@ -7849,22 +8045,27 @@ report reason Tap Connect to chat + 轻按连接进行聊天 No comment provided by engineer. Tap Connect to send request + 轻按连接来发送请求 No comment provided by engineer. Tap Connect to use bot + 轻按“连接”使用机器人 No comment provided by engineer. Tap Create SimpleX address in the menu to create it later. + 要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址” No comment provided by engineer. Tap Join group + 轻按加入群 No comment provided by engineer. @@ -7914,6 +8115,7 @@ report reason Test notifications + 测试通知 No comment provided by engineer. @@ -7955,6 +8157,7 @@ It can happen because of some bug or when the connection is compromised. The address will be short, and your profile will be shared via the address. + 地址不会长,将通过该简短地址分享个人资料。 alert message @@ -7964,6 +8167,7 @@ It can happen because of some bug or when the connection is compromised. The app protects your privacy by using different operators in each conversation. + 应用通过在每个对话中使用不同运营方保护你的隐私。 No comment provided by engineer. @@ -7983,6 +8187,7 @@ It can happen because of some bug or when the connection is compromised. The connection reached the limit of undelivered messages, your contact may be offline. + 连接达到了未送达消息上限,你的联系人可能处于离线状态。 No comment provided by engineer. @@ -8017,6 +8222,7 @@ It can happen because of some bug or when the connection is compromised. The link will be short, and group profile will be shared via the link. + 链接不会长,群资料会通过短链接分享。 alert message @@ -8050,6 +8256,7 @@ It can happen because of some bug or when the connection is compromised. The second preset operator in the app! + 应用中的第二个预设运营方! No comment provided by engineer. @@ -8078,6 +8285,7 @@ It can happen because of some bug or when the connection is compromised. The uploaded database archive will be permanently removed from the servers. + 已上传的数据库归档将会从服务器中永久移除。 No comment provided by engineer. @@ -8087,6 +8295,7 @@ It can happen because of some bug or when the connection is compromised. These conditions will also apply for: **%@**. + 这些条件将同样适用于: **%@**。 No comment provided by engineer. @@ -8111,6 +8320,7 @@ It can happen because of some bug or when the connection is compromised. This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + 此操作无法撤销 —— 比此聊天中所选消息更早发出并收到的消息将被删除。 alert message @@ -8150,6 +8360,7 @@ It can happen because of some bug or when the connection is compromised. This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + 此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。 No comment provided by engineer. @@ -8159,6 +8370,7 @@ It can happen because of some bug or when the connection is compromised. This message was deleted or not received yet. + 此消息被删除或尚未收到。 No comment provided by engineer. @@ -8168,10 +8380,12 @@ It can happen because of some bug or when the connection is compromised. This setting is for your current profile **%@**. + 此设置用于当前个人资料 **%@**。 No comment provided by engineer. Time to disappear is set only for new contacts. + 只为新联系人设置了消失时间。 No comment provided by engineer. @@ -8201,6 +8415,7 @@ It can happen because of some bug or when the connection is compromised. To protect against your link being replaced, you can compare contact security codes. + 为了防止链接被替换,你可以比较联系人安全代码。 No comment provided by engineer. @@ -8227,14 +8442,17 @@ You will be prompted to complete authentication before this feature is enabled.< To receive + 消息接收 No comment provided by engineer. To record speech please grant permission to use Microphone. + 为了记录语音请授予使用麦克风权限。 No comment provided by engineer. To record video please grant permission to use Camera. + 为了录制视频请授予使用相机权限。 No comment provided by engineer. @@ -8249,10 +8467,12 @@ You will be prompted to complete authentication before this feature is enabled.< To send + 发送 No comment provided by engineer. To send commands you must be connected. + 你必须已连接才能发送命令。 alert message @@ -8262,10 +8482,12 @@ You will be prompted to complete authentication before this feature is enabled.< To use another profile after connection attempt, delete the chat and use the link again. + 要在连接尝试后使用不同的个人资料,请删除聊天并再次使用该链接。 alert message To use the servers of **%@**, accept conditions of use. + 要使用**%@**的服务器,需接受条款。 No comment provided by engineer. @@ -8309,6 +8531,7 @@ You will be prompted to complete authentication before this feature is enabled.< Trying to connect to the server used to receive messages from this connection. + 尝试连接到用于从该连接接收消息的服务器。 subscription status explanation @@ -8358,6 +8581,7 @@ You will be prompted to complete authentication before this feature is enabled.< Undelivered messages + 未送达的消息 No comment provided by engineer. @@ -8454,6 +8678,7 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link + 不支持的连接链接 No comment provided by engineer. @@ -8483,6 +8708,7 @@ To connect, please ask your contact to create another connection link and check Updated conditions + 条款已更新 No comment provided by engineer. @@ -8492,14 +8718,17 @@ To connect, please ask your contact to create another connection link and check Upgrade + 升级 alert button Upgrade address + 升级地址 No comment provided by engineer. Upgrade address? + 升级地址? alert message @@ -8509,14 +8738,17 @@ To connect, please ask your contact to create another connection link and check Upgrade group link? + 升级群链接? alert message Upgrade link + 升级链接 No comment provided by engineer. Upgrade your address + 升级你的地址 No comment provided by engineer. @@ -8551,6 +8783,7 @@ To connect, please ask your contact to create another connection link and check Use %@ + 使用 %@ No comment provided by engineer. @@ -8560,6 +8793,7 @@ To connect, please ask your contact to create another connection link and check Use SOCKS proxy + 使用 SOCKS 代理 No comment provided by engineer. @@ -8569,10 +8803,12 @@ To connect, please ask your contact to create another connection link and check Use TCP port %@ when no port is specified. + 当未指定端口时使用TCP端口%@。 No comment provided by engineer. Use TCP port 443 for preset servers only. + 仅预设服务器使用 TCP 协议 443 端口。 No comment provided by engineer. @@ -8587,10 +8823,12 @@ To connect, please ask your contact to create another connection link and check Use for files + 用于文件 No comment provided by engineer. Use for messages + 用于消息 No comment provided by engineer. @@ -8610,6 +8848,7 @@ To connect, please ask your contact to create another connection link and check Use incognito profile + 使用隐身个人资料 No comment provided by engineer. @@ -8639,6 +8878,7 @@ To connect, please ask your contact to create another connection link and check Use servers + 使用服务器 No comment provided by engineer. @@ -8653,6 +8893,7 @@ To connect, please ask your contact to create another connection link and check Use web port + 使用 web 端口 No comment provided by engineer. @@ -8662,6 +8903,7 @@ To connect, please ask your contact to create another connection link and check Username + 用户名 No comment provided by engineer. @@ -8729,6 +8971,10 @@ To connect, please ask your contact to create another connection link and check 视频将在您的联系人在线时收到,请稍等或稍后查看! No comment provided by engineer. + + Videos + No comment provided by engineer. + Videos and files up to 1gb 最大 1gb 的视频和文件 @@ -8736,6 +8982,7 @@ To connect, please ask your contact to create another connection link and check View conditions + 查看条款 No comment provided by engineer. @@ -8745,6 +8992,7 @@ To connect, please ask your contact to create another connection link and check View updated conditions + 查看更新后的条款 No comment provided by engineer. @@ -8844,6 +9092,7 @@ To connect, please ask your contact to create another connection link and check Welcome your contacts 👋 + 欢迎联系人👋 No comment provided by engineer. @@ -8863,6 +9112,7 @@ To connect, please ask your contact to create another connection link and check When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + 当启用了超过一个运营方时,没有一个运营方拥有了解谁和谁联络的元数据。 No comment provided by engineer. @@ -8962,6 +9212,7 @@ To connect, please ask your contact to create another connection link and check You are already connected with %@. + 你已经与%@保持连接。 No comment provided by engineer. @@ -8998,6 +9249,7 @@ Repeat join request? You are connected to the server used to receive messages from this connection. + 你已连接到用于接收该连接消息的服务器。 subscription status explanation @@ -9007,6 +9259,7 @@ Repeat join request? You are not connected to the server used to receive messages from this connection (no subscription). + 未连接到用于从该连接接收消息的服务器(无订阅)。 subscription status explanation @@ -9026,6 +9279,7 @@ Repeat join request? You can configure servers via settings. + 你可以通过设置配置服务器。 No comment provided by engineer. @@ -9070,6 +9324,7 @@ Repeat join request? You can set connection name, to remember who the link was shared with. + 你可以设置连接名称,用来记住和谁分享了这个链接。 No comment provided by engineer. @@ -9114,6 +9369,7 @@ Repeat join request? You can view your reports in Chat with admins. + 你可以在和管理员和聊天中查看你的举报。 alert message @@ -9199,6 +9455,7 @@ Repeat connection request? You will be able to send messages **only after your request is accepted**. + **只有在你的请求被接受后**你才能发送消息。 No comment provided by engineer. @@ -9233,6 +9490,7 @@ Repeat connection request? You will stop receiving messages from this chat. Chat history will be preserved. + 你将停止从这个聊天收到消息。聊天历史将被保留。 No comment provided by engineer. @@ -9267,6 +9525,7 @@ Repeat connection request? Your business contact + 你的企业联系人 No comment provided by engineer. @@ -9286,6 +9545,7 @@ Repeat connection request? Your chat preferences + 你的聊天偏好设置 alert title @@ -9303,6 +9563,7 @@ Repeat connection request? Your contact + 你的联系人 No comment provided by engineer. @@ -9322,6 +9583,7 @@ Repeat connection request? Your credentials may be sent unencrypted. + 你的凭据可能以未经加密的方式被发送。 No comment provided by engineer. @@ -9336,6 +9598,7 @@ Repeat connection request? Your group + 你的群 No comment provided by engineer. @@ -9370,6 +9633,7 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + 您的个人资料已修改。如果进行保存,更新后的个人资料将发送到所有联系人。 alert message @@ -9384,6 +9648,7 @@ Repeat connection request? Your servers + 你的服务器 No comment provided by engineer. @@ -9432,10 +9697,12 @@ Repeat connection request? accepted invitation + 已接受邀请 chat list item title accepted you + 接受了你 rcv group event chat item @@ -9460,6 +9727,7 @@ Repeat connection request? all + 全部 member criteria value @@ -9479,6 +9747,7 @@ Repeat connection request? archived report + 已存档的举报 No comment provided by engineer. @@ -9549,6 +9818,7 @@ marked deleted chat item preview text can't send messages + 无法发送消息 No comment provided by engineer. @@ -9653,10 +9923,12 @@ marked deleted chat item preview text contact deleted + 删除了联系人 No comment provided by engineer. contact disabled + 禁用了联系人 No comment provided by engineer. @@ -9671,10 +9943,12 @@ marked deleted chat item preview text contact not ready + 联系人未就绪 No comment provided by engineer. contact should accept… + 联系人应当接受… No comment provided by engineer. @@ -9845,6 +10119,7 @@ pref value group + shown on group welcome message @@ -9854,6 +10129,7 @@ pref value group is deleted + 群被删除了 No comment provided by engineer. @@ -9978,6 +10254,7 @@ pref value member has old version + 成员有旧版本 No comment provided by engineer. @@ -10012,6 +10289,7 @@ pref value moderator + 协管 member role @@ -10041,6 +10319,7 @@ pref value no subscription + 无订阅 No comment provided by engineer. @@ -10050,6 +10329,7 @@ pref value not synchronized + 未同步 No comment provided by engineer. @@ -10111,10 +10391,12 @@ time to disappear pending approval + 待批准 No comment provided by engineer. pending review + 待审核 No comment provided by engineer. @@ -10134,6 +10416,7 @@ time to disappear rejected + 被拒绝 No comment provided by engineer. @@ -10158,6 +10441,7 @@ time to disappear removed from group + 从群被删除了 No comment provided by engineer. @@ -10172,30 +10456,37 @@ time to disappear request is sent + 发送了请求 No comment provided by engineer. request to join rejected + 加入请求被拒绝 No comment provided by engineer. requested connection + 已请求连接 rcv group event chat item requested connection from group %@ + 来自群组%@的已请求连接 rcv direct event chat item requested to connect + 被请求连接 chat list item title review + 审核 No comment provided by engineer. reviewed by admins + 由管理员审核 No comment provided by engineer. @@ -10384,6 +10675,7 @@ last received msg: %2$@ you accepted this member + 你接受了该成员 snd group event chat item @@ -10519,22 +10811,27 @@ last received msg: %2$@ %d new events + %d条新事件 notification body From %d chat(s) + 来自 %d 条聊天 notification body From: %@ + 来自: %@ notification body New events + 新事件 notification New messages + 新消息 notification diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 5d619ac130..25df063f82 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -5,6 +5,7 @@ // Created by Evgeny on 26/04/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // +// Spec: spec/services/notifications.md import UserNotifications import OSLog @@ -22,6 +23,7 @@ let nseSuspendSchedule: SuspendSchedule = (2, 4) let fastNSESuspendSchedule: SuspendSchedule = (1, 1) +// Spec: spec/services/notifications.md#NSENotificationData public enum NSENotificationData { case connectionEvent(_ user: User, _ connEntity: ConnectionEntity) case contactConnected(_ user: any UserLike, _ contact: Contact) @@ -76,6 +78,7 @@ public enum NSENotificationData { // Once the last thread in the process completes processing chat controller is suspended, and the database is closed, to avoid // background crashes and contention for database with the application (both UI and background fetch triggered either on schedule // or when background notification is received. +// Spec: spec/services/notifications.md#NSEThreads class NSEThreads { static let shared = NSEThreads() private let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock") @@ -238,6 +241,7 @@ class NSEThreads { // NotificationEntities for the same connection across multiple NSE instances (NSEThreads) are processed sequentially, so that the earliest NSE instance receives the earliest messages. // The reason for this complexity is to process all required messages within allotted 30 seconds, // accounting for the possibility that multiple notifications may be delivered concurrently. +// Spec: spec/services/notifications.md#NotificationEntity struct NotificationEntity { var ntfConn: NtfConn var entityId: ChatId @@ -279,6 +283,7 @@ struct NotificationEntity { // Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never // more than one process of notification service extension exists at a time. // Soon after notification service delivers the last notification it is either suspended or terminated. +// Spec: spec/services/notifications.md#NotificationService class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? // served as notification if no message attempts (msgBestAttemptNtf) could be produced @@ -291,6 +296,7 @@ class NotificationService: UNNotificationServiceExtension { var appSubscriber: AppSubscriber? var returnedSuspension = false + // Spec: spec/services/notifications.md#didReceive override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("DEBUGGING: NotificationService.didReceive") let receivedNtf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() } @@ -594,6 +600,7 @@ class NotificationService: UNNotificationServiceExtension { serviceBestAttemptNtf = ntf } + // Spec: spec/services/notifications.md#deliverBestAttemptNtf private func deliverBestAttemptNtf(urgent: Bool = false) { logger.debug("NotificationService.deliverBestAttemptNtf urgent: \(urgent) expectingMoreMessages: \(self.expectingMoreMessages)") if let handler = contentHandler, urgent || !expectingMoreMessages { @@ -770,6 +777,7 @@ class NotificationService: UNNotificationServiceExtension { } // nseStateGroupDefault must not be used in NSE directly, only via this singleton +// Spec: spec/services/notifications.md#NSEChatState class NSEChatState { static let shared = NSEChatState() private var value_ = NSEState.created @@ -824,6 +832,7 @@ var networkConfig: NetCfg = getNetCfg() // startChat uses semaphore startLock to ensure that only one didReceive thread can start chat controller // Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active + // Spec: spec/services/notifications.md#startChat-NSE func startChat() -> DBMigrationResult? { logger.debug("NotificationService: startChat") // only skip creating if there is chat controller @@ -848,6 +857,7 @@ func startChat() -> DBMigrationResult? { } } + // Spec: spec/services/notifications.md#doStartChat func doStartChat() -> DBMigrationResult? { logger.debug("NotificationService: doStartChat") haskell_init_nse() @@ -940,6 +950,7 @@ func chatSuspended() { // A single loop is used per Notification service extension process to receive and process all messages depending on the NSE state // If the extension is not active yet, or suspended/suspending, or the app is running, the notifications will not be received. + // Spec: spec/services/notifications.md#receiveMessages func receiveMessages() async { logger.debug("NotificationService receiveMessages") while true { @@ -988,6 +999,7 @@ private let isInChina = SKStorefront().countryCode == "CHN" private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } @inline(__always) + // Spec: spec/services/notifications.md#receivedMsgNtf func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)? { logger.debug("NotificationService receivedMsgNtf: \(res.responseType)") switch res { diff --git a/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings index 5ef592ec70..4e4b130fa4 100644 --- a/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d条新事件"; + +/* notification body */ +"From %d chat(s)" = "来自 %d 条聊天"; + +/* notification body */ +"From: %@" = "来自: %@"; + +/* notification */ +"New events" = "新事件"; + +/* notification */ +"New messages" = "新消息"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings index 2fedf0e6f1..dfb7a302b9 100644 --- a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -14,10 +14,10 @@ "Cannot forward message" = "Nem lehet továbbítani az üzenetet"; /* No comment provided by engineer. */ -"Comment" = "Hozzászólás"; +"Comment" = "Megjegyzés"; /* No comment provided by engineer. */ -"Currently maximum supported file size is %@." = "Jelenleg támogatott legnagyobb fájl méret: %@."; +"Currently maximum supported file size is %@." = "Jelenleg támogatott legnagyobb fájlméret: %@."; /* No comment provided by engineer. */ "Database downgrade required" = "Adatbázis visszafejlesztése szükséges"; @@ -80,7 +80,7 @@ "Please create a profile in the SimpleX app" = "Hozzon létre egy profilt a SimpleX alkalmazásban"; /* No comment provided by engineer. */ -"Selected chat preferences prohibit this message." = "A kijelölt csevegési beállítások tiltják ezt az üzenetet."; +"Selected chat preferences prohibit this message." = "A kiválasztott csevegési beállítások tiltják ezt az üzenetet."; /* No comment provided by engineer. */ "Sending a message takes longer than expected." = "Az üzenet elküldése a vártnál tovább tart."; diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index fa9a4efdf7..9265138c53 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a */, ); path = Libraries; sourceTree = ""; @@ -2003,7 +2003,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2053,7 +2053,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2095,7 +2095,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2115,7 +2115,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2140,7 +2140,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2177,7 +2177,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2214,7 +2214,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2265,7 +2265,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2316,7 +2316,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2350,7 +2350,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 321; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 40cee93faf..85c84a6f45 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -110,6 +110,7 @@ public func resetChatCtrl() { migrationResult = nil } +// Spec: spec/api.md#sendSimpleXCmd @inline(__always) public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil, retryNum: Int32 = 0) -> APIResult { if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl, retryNum: retryNum) { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index fce0f100f2..b31a799e68 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -5,6 +5,7 @@ // Created by Evgeny on 26/04/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // +// Spec: spec/api.md import Foundation import SwiftUI @@ -22,6 +23,7 @@ public func onOff(_ b: Bool) -> String { b ? "on" : "off" } +// Spec: spec/api.md#APIResult public enum APIResult: Decodable where R: Decodable, R: ChatAPIResult { case result(R) case error(ChatError) @@ -59,6 +61,7 @@ public enum APIResult: Decodable where R: Decodable, R: ChatAPIResult { } } +// Spec: spec/api.md#ChatAPIResult public protocol ChatAPIResult: Decodable { var responseType: String { get } var details: String { get } @@ -79,6 +82,7 @@ extension ChatAPIResult { } } +// Spec: spec/api.md#decodeAPIResult public func decodeAPIResult(_ d: Data) -> APIResult { // print("decodeAPIResult \(String(describing: R.self))") do { @@ -691,6 +695,7 @@ private func encodeCJSON(_ value: T) -> [CChar] { encodeJSON(value).cString(using: .utf8)! } +// Spec: spec/api.md#ChatError public enum ChatError: Decodable, Hashable, Error { case error(errorType: ChatErrorType) case errorAgent(agentError: AgentErrorType) @@ -713,6 +718,7 @@ public enum ChatError: Decodable, Hashable, Error { } } +// Spec: spec/api.md#ChatErrorType public enum ChatErrorType: Decodable, Hashable { case noActiveUser case noConnectionUser(agentConnId: String) diff --git a/apps/ios/SimpleXChat/CallTypes.swift b/apps/ios/SimpleXChat/CallTypes.swift index da1720c134..ece65130e6 100644 --- a/apps/ios/SimpleXChat/CallTypes.swift +++ b/apps/ios/SimpleXChat/CallTypes.swift @@ -5,10 +5,12 @@ // Created by Evgeny on 05/05/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // +// Spec: spec/services/calls.md import Foundation import SwiftUI +// Spec: spec/services/calls.md#WebRTCCallOffer public struct WebRTCCallOffer: Encodable { public init(callType: CallType, rtcSession: WebRTCSession) { self.callType = callType @@ -19,6 +21,7 @@ public struct WebRTCCallOffer: Encodable { public var rtcSession: WebRTCSession } +// Spec: spec/services/calls.md#WebRTCSession public struct WebRTCSession: Codable { public init(rtcSession: String, rtcIceCandidates: String) { self.rtcSession = rtcSession @@ -29,6 +32,7 @@ public struct WebRTCSession: Codable { public var rtcIceCandidates: String } +// Spec: spec/services/calls.md#WebRTCExtraInfo public struct WebRTCExtraInfo: Codable { public init(rtcIceCandidates: String) { self.rtcIceCandidates = rtcIceCandidates @@ -37,6 +41,7 @@ public struct WebRTCExtraInfo: Codable { public var rtcIceCandidates: String } +// Spec: spec/services/calls.md#RcvCallInvitation public struct RcvCallInvitation: Decodable { public var user: User public var contact: Contact @@ -65,6 +70,7 @@ public struct RcvCallInvitation: Decodable { ) } +// Spec: spec/services/calls.md#CallType public struct CallType: Codable { public init(media: CallMediaType, capabilities: CallCapabilities) { self.media = media diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5d1d5b4302..d95e5233c1 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -5,6 +5,7 @@ // Created by Evgeny on 26/04/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // +// Spec: spec/state.md | spec/api.md import Foundation import SwiftUI @@ -1367,6 +1368,7 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable { } } +// Spec: spec/state.md#ChatInfo public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case direct(contact: Contact) case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?) @@ -1871,6 +1873,7 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike { } } +// Spec: spec/state.md#ChatStats public struct ChatStats: Decodable, Hashable { public init( unreadCount: Int = 0, @@ -2110,6 +2113,11 @@ public struct Connection: Decodable, Hashable { public var id: ChatId { get { ":\(connId)" } } + public var connFailedErr: String? { + if case let .failed(err) = connStatus { return err } + return nil + } + public var connDisabled: Bool { authErrCounter >= 10 // authErrDisableCount in core } @@ -2295,15 +2303,16 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable { } } -public enum ConnStatus: String, Decodable, Hashable { - case new = "new" - case prepared = "prepared" - case joined = "joined" - case requested = "requested" - case accepted = "accepted" - case sndReady = "snd-ready" - case ready = "ready" - case deleted = "deleted" +public enum ConnStatus: Decodable, Hashable { + case new + case prepared + case joined + case requested + case accepted + case sndReady + case ready + case deleted + case failed(connError: String) var initiated: Bool? { get { @@ -2316,6 +2325,7 @@ public enum ConnStatus: String, Decodable, Hashable { case .sndReady: return nil case .ready: return nil case .deleted: return nil + case .failed: return nil } } } @@ -4234,6 +4244,7 @@ public struct CIFile: Decodable, Hashable { } } +// Spec: spec/services/files.md#CryptoFile public struct CryptoFile: Codable, Hashable { public var filePath: String // the name of the file, not a full path public var cryptoArgs: CryptoFileArgs? @@ -4281,6 +4292,7 @@ public struct CryptoFile: Codable, Hashable { static var decryptedUrls = Dictionary() } +// Spec: spec/services/files.md#CryptoFileArgs public struct CryptoFileArgs: Codable, Hashable { public var fileKey: String public var fileNonce: String @@ -4601,7 +4613,7 @@ extension MsgContent: Encodable { } } -public enum MsgContentTag: String { +public enum MsgContentTag: String, Decodable { case text case link case image @@ -4651,6 +4663,7 @@ public enum Format: Decodable, Equatable, Hashable { case strikeThrough case snippet case secret + case small case colored(color: FormatColor) case uri case hyperLink(showText: String?, linkUri: String) diff --git a/apps/ios/SimpleXChat/CryptoFile.swift b/apps/ios/SimpleXChat/CryptoFile.swift index dfe833f832..5a0d48dced 100644 --- a/apps/ios/SimpleXChat/CryptoFile.swift +++ b/apps/ios/SimpleXChat/CryptoFile.swift @@ -4,6 +4,7 @@ // // Created by Evgeny on 05/09/2023. // Copyright © 2023 SimpleX Chat. All rights reserved. +// Spec: spec/services/files.md // import Foundation @@ -13,6 +14,7 @@ enum WriteFileResult: Decodable { case error(writeError: String) } +// Spec: spec/services/files.md#writeCryptoFile public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs { let ptr: UnsafeMutableRawPointer = malloc(data.count) memcpy(ptr, (data as NSData).bytes, data.count) @@ -25,6 +27,7 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs { } } +// Spec: spec/services/files.md#readCryptoFile public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> Data { var cPath = path.cString(using: .utf8)! var cKey = cryptoArgs.fileKey.cString(using: .utf8)! @@ -47,6 +50,7 @@ public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> D } } +// Spec: spec/services/files.md#encryptCryptoFile public func encryptCryptoFile(fromPath: String, toPath: String) throws -> CryptoFileArgs { var cFromPath = fromPath.cString(using: .utf8)! var cToPath = toPath.cString(using: .utf8)! @@ -58,6 +62,7 @@ public func encryptCryptoFile(fromPath: String, toPath: String) throws -> Crypto } } +// Spec: spec/services/files.md#decryptCryptoFile public func decryptCryptoFile(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) throws { var cFromPath = fromPath.cString(using: .utf8)! var cKey = cryptoArgs.fileKey.cString(using: .utf8)! diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 2341eb4a4f..3d0dd663c1 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -5,6 +5,7 @@ // Created by JRoberts on 15.04.2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // +// Spec: spec/services/files.md import Foundation import OSLog @@ -13,14 +14,19 @@ import UIKit let logger = Logger() // image file size for complession +// Spec: spec/services/files.md#MAX_IMAGE_SIZE public let MAX_IMAGE_SIZE: Int64 = 261_120 // 255KB +// Spec: spec/services/files.md#MAX_IMAGE_SIZE_AUTO_RCV public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2 +// Spec: spec/services/files.md#MAX_VOICE_SIZE_AUTO_RCV public let MAX_VOICE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2 +// Spec: spec/services/files.md#MAX_VIDEO_SIZE_AUTO_RCV public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023KB +// Spec: spec/services/files.md#MAX_FILE_SIZE_XFTP public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1GB public let MAX_FILE_SIZE_LOCAL: Int64 = Int64.max @@ -37,10 +43,12 @@ private let CHAT_DB_BAK: String = "_chat.db.bak" private let AGENT_DB_BAK: String = "_agent.db.bak" +// Spec: spec/database.md#getDocumentsDirectory public func getDocumentsDirectory() -> URL { FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! } +// Spec: spec/database.md#getGroupContainerDirectory public func getGroupContainerDirectory() -> URL { FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)! } @@ -51,12 +59,14 @@ func getAppDirectory() -> URL { : getDocumentsDirectory() } +// Spec: spec/database.md#DB_FILE_PREFIX let DB_FILE_PREFIX = "simplex_v1" func getLegacyDatabasePath() -> URL { getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false) } +// Spec: spec/database.md#getAppDatabasePath public func getAppDatabasePath() -> URL { dbContainerGroupDefault.get() == .group ? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false) @@ -72,6 +82,7 @@ func fileModificationDate(_ path: String) -> Date? { } } +// Spec: spec/services/files.md#deleteAppDatabaseAndFiles public func deleteAppDatabaseAndFiles() { let fm = FileManager.default let dbPath = getAppDatabasePath().path @@ -93,6 +104,7 @@ public func deleteAppDatabaseAndFiles() { storeDBPassphraseGroupDefault.set(true) } +// Spec: spec/services/files.md#deleteAppFiles public func deleteAppFiles() { let fm = FileManager.default do { @@ -183,6 +195,7 @@ public func removeLegacyDatabaseAndFiles() -> Bool { return r1 && r2 } +// Spec: spec/services/files.md#getTempFilesDirectory public func getTempFilesDirectory() -> URL { getAppDirectory().appendingPathComponent("temp_files", isDirectory: true) } @@ -191,6 +204,7 @@ public func getMigrationTempFilesDirectory() -> URL { getDocumentsDirectory().appendingPathComponent("migration_temp_files", isDirectory: true) } +// Spec: spec/services/files.md#getAppFilesDirectory public func getAppFilesDirectory() -> URL { getAppDirectory().appendingPathComponent("app_files", isDirectory: true) } @@ -199,6 +213,7 @@ public func getAppFilePath(_ fileName: String) -> URL { getAppFilesDirectory().appendingPathComponent(fileName) } +// Spec: spec/services/files.md#getWallpaperDirectory public func getWallpaperDirectory() -> URL { getAppDirectory().appendingPathComponent("assets", isDirectory: true).appendingPathComponent("wallpapers", isDirectory: true) } @@ -207,6 +222,7 @@ public func getWallpaperFilePath(_ filename: String) -> URL { getWallpaperDirectory().appendingPathComponent(filename) } +// Spec: spec/services/files.md#saveFile public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> CryptoFile? { let filePath = getAppFilePath(fileName) do { @@ -223,6 +239,7 @@ public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> Crypt } } +// Spec: spec/services/files.md#removeFile public func removeFile(_ url: URL) { do { try FileManager.default.removeItem(atPath: url.path) @@ -239,12 +256,14 @@ public func removeFile(_ fileName: String) { } } +// Spec: spec/services/files.md#cleanupDirectFile public func cleanupDirectFile(_ aChatItem: AChatItem) { if aChatItem.chatInfo.chatType == .direct { cleanupFile(aChatItem) } } +// Spec: spec/services/files.md#cleanupFile public func cleanupFile(_ aChatItem: AChatItem) { let cItem = aChatItem.chatItem let mc = cItem.content.msgContent diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 31b7ef83ff..24dc58202a 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -5,6 +5,7 @@ // Created by Evgeny on 28/04/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // +// Spec: spec/services/notifications.md import Foundation import UserNotifications @@ -22,6 +23,7 @@ public let appNotificationId = "chat.simplex.app.notification" let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification") +// Spec: spec/services/notifications.md#createContactRequestNtf public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( @@ -40,6 +42,7 @@ public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: User ) } +// Spec: spec/services/notifications.md#createContactConnectedNtf public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( @@ -59,6 +62,7 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, ) } +// Spec: spec/services/notifications.md#createMessageReceivedNtf public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem, _ badgeCount: Int) -> UNMutableNotificationContent { let previewMode = ntfPreviewModeGroupDefault.get() var title: String @@ -78,6 +82,7 @@ public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ ) } +// Spec: spec/services/notifications.md#createCallInvitationNtf public func createCallInvitationNtf(_ invitation: RcvCallInvitation, _ badgeCount: Int) -> UNMutableNotificationContent { let text = invitation.callType.media == .video ? NSLocalizedString("Incoming video call", comment: "notification") @@ -93,6 +98,7 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation, _ badgeCoun ) } +// Spec: spec/services/notifications.md#createConnectionEventNtf public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden var title: String @@ -124,6 +130,7 @@ public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntit ) } +// Spec: spec/services/notifications.md#createErrorNtf public func createErrorNtf(_ dbStatus: DBMigrationResult, _ badgeCount: Int) -> UNMutableNotificationContent { var title: String switch dbStatus { @@ -149,6 +156,7 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult, _ badgeCount: Int) -> ) } +// Spec: spec/services/notifications.md#createAppStoppedNtf public func createAppStoppedNtf(_ badgeCount: Int) -> UNMutableNotificationContent { return createNotification( categoryIdentifier: ntfCategoryConnectionEvent, @@ -163,6 +171,7 @@ private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember : "#\(groupInfo.displayName) \(groupMember.chatViewName):" } +// Spec: spec/services/notifications.md#createNotification public func createNotification( categoryIdentifier: String, title: String, @@ -187,6 +196,7 @@ public func createNotification( return content } +// Spec: spec/services/notifications.md#hideSecrets func hideSecrets(_ cItem: ChatItem) -> String { if let md = cItem.formattedText { var res = "" diff --git a/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift b/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift index 662f8b43d1..2b64627dc2 100644 --- a/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift +++ b/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI +// Spec: spec/services/theme.md#PresetWallpaper public enum PresetWallpaper: CaseIterable { case cats case flowers @@ -306,6 +307,7 @@ public enum WallpaperScaleType: String, Codable, CaseIterable { } } +// Spec: spec/services/theme.md#WallpaperType public enum WallpaperType: Equatable { public var image: SwiftUI.Image? { if let uiImage { diff --git a/apps/ios/SimpleXChat/Theme/ThemeTypes.swift b/apps/ios/SimpleXChat/Theme/ThemeTypes.swift index 4074382543..a4e8050c6e 100644 --- a/apps/ios/SimpleXChat/Theme/ThemeTypes.swift +++ b/apps/ios/SimpleXChat/Theme/ThemeTypes.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI +// Spec: spec/services/theme.md#DefaultTheme public enum DefaultTheme: String, Codable, Equatable { case LIGHT case DARK @@ -39,6 +40,7 @@ public enum DefaultThemeMode: String, Codable { case dark } +// Spec: spec/services/theme.md#Colors public class Colors: ObservableObject, NSCopying, Equatable { @Published public var primary: Color @Published public var primaryVariant: Color @@ -84,6 +86,7 @@ public class Colors: ObservableObject, NSCopying, Equatable { public func clone() -> Colors { copy() as! Colors } } +// Spec: spec/services/theme.md#AppColors public class AppColors: ObservableObject, NSCopying, Equatable { @Published public var title: Color @Published public var primaryVariant2: Color @@ -135,6 +138,7 @@ public class AppColors: ObservableObject, NSCopying, Equatable { } } +// Spec: spec/services/theme.md#AppWallpaper public class AppWallpaper: ObservableObject, NSCopying, Equatable { public static func == (lhs: AppWallpaper, rhs: AppWallpaper) -> Bool { lhs.background == rhs.background && @@ -222,6 +226,7 @@ public enum ThemeColor { } } +// Spec: spec/services/theme.md#ThemeColors public struct ThemeColors: Codable, Equatable, Hashable { public var primary: String? = nil public var primaryVariant: String? = nil @@ -293,6 +298,7 @@ public struct ThemeColors: Codable, Equatable, Hashable { } } +// Spec: spec/services/theme.md#ThemeWallpaper public struct ThemeWallpaper: Codable, Equatable, Hashable { public var preset: String? public var scale: Float? @@ -375,6 +381,7 @@ public struct ThemeWallpaper: Codable, Equatable, Hashable { /// If you add new properties, make sure they serialized to YAML correctly, see: /// encodeThemeOverrides() +// Spec: spec/services/theme.md#ThemeOverrides public struct ThemeOverrides: Codable, Equatable, Hashable { public var themeId: String = UUID().uuidString public var base: DefaultTheme @@ -559,6 +566,7 @@ extension [ThemeOverrides] { } +// Spec: spec/services/theme.md#ThemeModeOverrides public struct ThemeModeOverrides: Codable, Hashable { public var light: ThemeModeOverride? = nil public var dark: ThemeModeOverride? = nil @@ -573,6 +581,7 @@ public struct ThemeModeOverrides: Codable, Hashable { } } +// Spec: spec/services/theme.md#ThemeModeOverride public struct ThemeModeOverride: Codable, Equatable, Hashable { public var mode: DefaultThemeMode// = CurrentColors.base.mode public var colors: ThemeColors = ThemeColors() diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 038546f889..fb8529fb88 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -1613,7 +1613,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Изтрий съобщението?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Изтрий съобщенията"; /* No comment provided by engineer. */ @@ -2750,7 +2751,7 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Ролята на члена ще бъде променена на \"%@\". Членът ще получи нова покана."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Членът ще бъде премахнат от групата - това не може да бъде отменено!"; /* No comment provided by engineer. */ @@ -3417,13 +3418,13 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Relay сървърът защитава вашия IP адрес, но може да наблюдава продължителността на разговора."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Премахване"; /* No comment provided by engineer. */ "Remove member" = "Острани член"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Острани член?"; /* No comment provided by engineer. */ diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index dd486001c7..33ad97d821 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -1258,7 +1258,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Smazat zprávu?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Smazat zprávy"; /* No comment provided by engineer. */ @@ -2193,7 +2194,7 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Role člena se změní na \"%@\". Člen obdrží novou pozvánku."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Člen bude odstraněn ze skupiny - toto nelze vzít zpět!"; /* No comment provided by engineer. */ @@ -2728,13 +2729,13 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Přenosový server chrání vaši IP adresu, ale může sledovat dobu trvání hovoru."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Odstranit"; /* No comment provided by engineer. */ "Remove member" = "Odstranit člena"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Odebrat člena?"; /* No comment provided by engineer. */ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index caf58399de..f305aca473 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1733,7 +1733,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Die Nachricht löschen?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Nachrichten löschen"; /* No comment provided by engineer. */ @@ -3308,10 +3309,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Die Mitgliederrolle wird auf \"%@\" geändert. Das Mitglied wird eine neue Einladung erhalten."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!"; /* alert message */ @@ -4380,7 +4381,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Relais-Server schützen Ihre IP-Adresse, aber sie können die Anrufdauer erfassen."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Entfernen"; /* No comment provided by engineer. */ @@ -4395,7 +4396,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Mitglied entfernen"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Das Mitglied entfernen?"; /* No comment provided by engineer. */ diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 5ce7ab6843..9ac7628abb 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -384,7 +384,7 @@ swipe action */ "accepted invitation" = "invitación aceptada"; /* rcv group event chat item */ -"accepted you" = "te ha aceptado"; +"accepted you" = "te ha admitido"; /* No comment provided by engineer. */ "Acknowledged" = "Confirmaciones"; @@ -1733,7 +1733,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "¿Eliminar mensaje?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Activar"; /* No comment provided by engineer. */ @@ -3308,10 +3309,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "El rol del miembro cambiará a \"%@\" y recibirá una invitación nueva."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "El miembro será eliminado del chat. ¡No puede deshacerse!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No puede deshacerse!"; /* alert message */ @@ -3654,7 +3655,7 @@ snd error text */ "No device token!" = "¡Sin dispositivo token!"; /* item status description */ -"No direct connection yet, message is forwarded by admin." = "Aún no hay conexión directa con este miembro, el mensaje es reenviado por el administrador."; +"No direct connection yet, message is forwarded by admin." = "Aún no hay conexión directa, los mensajes son reenviados por el administrador."; /* No comment provided by engineer. */ "no e2e encryption" = "sin cifrar"; @@ -4380,7 +4381,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Eliminar"; /* No comment provided by engineer. */ @@ -4395,7 +4396,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Expulsar miembro"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "¿Expulsar miembro?"; /* No comment provided by engineer. */ @@ -6052,7 +6053,7 @@ report reason */ "You accepted connection" = "Has aceptado la conexión"; /* snd group event chat item */ -"you accepted this member" = "has aceptado al miembro"; +"you accepted this member" = "has admitido al miembro"; /* No comment provided by engineer. */ "You allow" = "Permites"; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 884be40cc1..ea3f9c4386 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -943,7 +943,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Poista viesti?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Poista viestit"; /* No comment provided by engineer. */ @@ -1869,7 +1870,7 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Jäsenen rooli muutetaan muotoon \"%@\". Jäsen saa uuden kutsun."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Jäsen poistetaan ryhmästä - tätä ei voi perua!"; /* No comment provided by engineer. */ @@ -2398,13 +2399,13 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Poista"; /* No comment provided by engineer. */ "Remove member" = "Poista jäsen"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Poista jäsen?"; /* No comment provided by engineer. */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index dbfac375d1..2b2a1e98e5 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1661,7 +1661,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Supprimer le message ?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Supprimer les messages"; /* No comment provided by engineer. */ @@ -3104,10 +3105,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Le rôle du membre sera changé pour \"%@\". Ce membre recevra une nouvelle invitation."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "Le membre sera retiré de la discussion - cela ne peut pas être annulé !"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Ce membre sera retiré du groupe - impossible de revenir en arrière !"; /* No comment provided by engineer. */ @@ -4005,7 +4006,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Le serveur relais protège votre adresse IP, mais il peut observer la durée de l'appel."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Supprimer"; /* No comment provided by engineer. */ @@ -4017,7 +4018,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Retirer le membre"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Retirer ce membre ?"; /* No comment provided by engineer. */ diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 451bdfc699..fc2f796bfd 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -41,10 +41,10 @@ "**Create group**: to create a new group." = "**Csoport létrehozása:** új csoport létrehozásához."; /* No comment provided by engineer. */ -"**e2e encrypted** audio call" = "**e2e titkosított** hanghívás"; +"**e2e encrypted** audio call" = "**végpontok között titkosított** hanghívás"; /* No comment provided by engineer. */ -"**e2e encrypted** video call" = "**e2e titkosított** videóhívás"; +"**e2e encrypted** video call" = "**végpontok között titkosított** videóhívás"; /* No comment provided by engineer. */ "**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken meg lesz osztva a SimpleX Chat kiszolgálóval, de az nem, hogy hány partnere vagy üzenete van."; @@ -65,7 +65,7 @@ "**Scan / Paste link**: to connect via a link you received." = "**Hivatkozás beolvasása / beillesztése**: egy kapott hivatkozáson keresztüli kapcsolódáshoz."; /* No comment provided by engineer. */ -"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés:** Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges."; +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés:** Az azonnali leküldéses értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges."; /* No comment provided by engineer. */ "**Warning**: the archive will be removed." = "**Figyelmeztetés:** az archívum el lesz távolítva."; @@ -119,10 +119,10 @@ "%@ is connected!" = "%@ kapcsolódott!"; /* No comment provided by engineer. */ -"%@ is not verified" = "%@ nincs hitelesítve"; +"%@ is not verified" = "%@ nincs ellenőrizve"; /* No comment provided by engineer. */ -"%@ is verified" = "%@ hitelesítve"; +"%@ is verified" = "%@ ellenőrizve"; /* No comment provided by engineer. */ "%@ server" = "%@ kiszolgáló"; @@ -194,7 +194,7 @@ "%lld %@" = "%lld %@"; /* No comment provided by engineer. */ -"%lld contact(s) selected" = "%lld partner kijelölve"; +"%lld contact(s) selected" = "%lld partner kiválasztva"; /* No comment provided by engineer. */ "%lld file(s) with total size of %@" = "%lld fájl, %@ összméretben"; @@ -507,7 +507,7 @@ swipe action */ "All data is kept private on your device." = "Az összes adat privát módon van tárolva az eszközén."; /* No comment provided by engineer. */ -"All group members will remain connected." = "Az összes csoporttag kapcsolatban marad."; +"All group members will remain connected." = "Az összes csoporttag továbbra is kapcsolatban marad."; /* feature role */ "all members" = "összes tag"; @@ -534,10 +534,10 @@ swipe action */ "All servers" = "Összes kiszolgáló"; /* No comment provided by engineer. */ -"All your contacts will remain connected." = "Az összes partnerével kapcsolatban marad."; +"All your contacts will remain connected." = "Az összes partnerével továbbra is kapcsolatban marad."; /* No comment provided by engineer. */ -"All your contacts will remain connected. Profile update will be sent to your contacts." = "A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára."; +"All your contacts will remain connected. Profile update will be sent to your contacts." = "Az összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára."; /* No comment provided by engineer. */ "All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra."; @@ -609,7 +609,7 @@ swipe action */ "Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Az elküldött üzenetek végleges törlése engedélyezve van a partnerei számára. (24 óra)"; /* No comment provided by engineer. */ -"Allow your contacts to send disappearing messages." = "Az eltűnő üzenetek küldésének engedélyezése a partnerei számára."; +"Allow your contacts to send disappearing messages." = "Az eltűnő üzenetek küldése engedélyezve van a partnerei számára."; /* No comment provided by engineer. */ "Allow your contacts to send files and media." = "A fájlok és a médiatartalmak küldése engedélyezve van a partnerei számára."; @@ -630,10 +630,10 @@ swipe action */ "always" = "mindig"; /* No comment provided by engineer. */ -"Always use private routing." = "Mindig használjon privát útválasztást."; +"Always use private routing." = "Mindig legyen használva privát útválasztás."; /* No comment provided by engineer. */ -"Always use relay" = "Mindig használjon továbbítókiszolgálót"; +"Always use relay" = "Mindig legyen használva továbbítókiszolgáló"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik."; @@ -735,7 +735,7 @@ swipe action */ "Audio and video calls" = "Hang- és videóhívások"; /* No comment provided by engineer. */ -"audio call (not e2e encrypted)" = "hanghívás (nem e2e titkosított)"; +"audio call (not e2e encrypted)" = "hanghívás (végpontok között NEM titkosított)"; /* chat feature */ "Audio/video calls" = "Hang- és videóhívások"; @@ -819,10 +819,10 @@ swipe action */ "Better user experience" = "Továbbfejlesztett felhasználói élmény"; /* No comment provided by engineer. */ -"Bio" = "Névjegy"; +"Bio" = "Életrajz"; /* alert title */ -"Bio too large" = "A névjegy túl hosszú"; +"Bio too large" = "Az életrajz túl hosszú"; /* No comment provided by engineer. */ "Black" = "Fekete"; @@ -913,7 +913,7 @@ marked deleted chat item preview text */ "call" = "hívás"; /* No comment provided by engineer. */ -"Call already ended!" = "A hívás már befejeződött!"; +"Call already ended!" = "A hívás már véget ért!"; /* call status */ "call error" = "híváshiba"; @@ -1120,7 +1120,7 @@ set passcode view */ "Chinese and Spanish interface" = "Kínai és spanyol kezelőfelület"; /* No comment provided by engineer. */ -"Choose _Migrate from another device_ on the new device and scan QR code." = "Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközén és olvassa be a QR-kódot."; +"Choose _Migrate from another device_ on the new device and scan QR code." = "Válassza az _Átköltöztetés egy másik eszközről_ beállítást az új eszközén és olvassa be a QR-kódot."; /* No comment provided by engineer. */ "Choose file" = "Fájl kiválasztása"; @@ -1138,25 +1138,25 @@ set passcode view */ "Chunks uploaded" = "Feltöltött töredékek"; /* swipe action */ -"Clear" = "Kiürítés"; +"Clear" = "Ürítés"; /* No comment provided by engineer. */ -"Clear conversation" = "Üzenetek kiürítése"; +"Clear conversation" = "Üzenetek ürítése"; /* No comment provided by engineer. */ -"Clear conversation?" = "Kiüríti az üzeneteket?"; +"Clear conversation?" = "Üríti a beszélgetés üzeneteit?"; /* No comment provided by engineer. */ -"Clear group?" = "Kiüríti a csoportot?"; +"Clear group?" = "Üríti a csoport üzeneteit?"; /* No comment provided by engineer. */ -"Clear or delete group?" = "Csoport kiürítése vagy törlése?"; +"Clear or delete group?" = "Csoport ürítése vagy törlése?"; /* No comment provided by engineer. */ -"Clear private notes?" = "Kiüríti a privát jegyzeteket?"; +"Clear private notes?" = "Üríti a privát jegyzetek tartalmát?"; /* No comment provided by engineer. */ -"Clear verification" = "Hitelesítés törlése"; +"Clear verification" = "Ellenőrzés törlése"; /* No comment provided by engineer. */ "Color chats with the new themes." = "Csevegések színezése új témákkal."; @@ -1273,7 +1273,7 @@ set passcode view */ "Connect via link" = "Kapcsolódás egy hivatkozáson keresztül"; /* new chat sheet title */ -"Connect via one-time link" = "Kapcsolódás egyszer használható meghívón keresztül"; +"Connect via one-time link" = "Kapcsolódás az egyszer használható meghívón keresztül"; /* new chat action */ "Connect with %@" = "Kapcsolódás a következővel: %@"; @@ -1291,7 +1291,7 @@ set passcode view */ "Connected servers" = "Kapcsolódott kiszolgálók"; /* No comment provided by engineer. */ -"Connected to desktop" = "Kapcsolódva a számítógéphez"; +"Connected to desktop" = "Társítva a számítógéppel"; /* No comment provided by engineer. */ "connecting" = "kapcsolódás"; @@ -1312,7 +1312,7 @@ set passcode view */ "connecting (introduction invitation)" = "kapcsolódás (bemutatkozó meghívó)"; /* call status */ -"connecting call" = "kapcsolódási hívás…"; +"connecting call" = "hívás kapcsolása…"; /* No comment provided by engineer. */ "Connecting server…" = "Kapcsolódás a kiszolgálóhoz…"; @@ -1324,7 +1324,7 @@ set passcode view */ "Connecting to contact, please wait or check later!" = "Kapcsolódás a partnerhez, várjon vagy ellenőrizze később!"; /* No comment provided by engineer. */ -"Connecting to desktop" = "Kapcsolódás a számítógéphez"; +"Connecting to desktop" = "Társítás számítógéppel"; /* No comment provided by engineer. */ "connecting…" = "kapcsolódás…"; @@ -1399,10 +1399,10 @@ set passcode view */ "contact disabled" = "partner letiltva"; /* No comment provided by engineer. */ -"contact has e2e encryption" = "a partner e2e titkosítással rendelkezik"; +"contact has e2e encryption" = "a partner végpontok közötti titkosítással rendelkezik"; /* No comment provided by engineer. */ -"contact has no e2e encryption" = "a partner nem rendelkezik e2e titkosítással"; +"contact has no e2e encryption" = "a partner nem rendelkezik végpontok közötti titkosítással"; /* notification */ "Contact hidden:" = "Rejtett név:"; @@ -1486,7 +1486,7 @@ set passcode view */ "Create list" = "Lista létrehozása"; /* No comment provided by engineer. */ -"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Új profil létrehozása a [számítógép-alkalmazásban](https://simplex.chat/downloads/). 💻"; +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Új profil létrehozása a [számítógépes alkalmazásban](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ "Create profile" = "Profil létrehozása"; @@ -1656,7 +1656,7 @@ swipe action */ "Delete after" = "Törlés ennyi idő után"; /* No comment provided by engineer. */ -"Delete all files" = "Az összes fájl törlése"; +"Delete all files" = "Összes fájl törlése"; /* No comment provided by engineer. */ "Delete and notify contact" = "Törlés, és a partner értesítése"; @@ -1733,7 +1733,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Törli az üzenetet?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Üzenetek törlése"; /* No comment provided by engineer. */ @@ -1815,7 +1816,7 @@ swipe action */ "Desktop address" = "Számítógép címe"; /* No comment provided by engineer. */ -"Desktop app version %@ is not compatible with this app." = "A számítógép-alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással."; +"Desktop app version %@ is not compatible with this app." = "A számítógépes alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással."; /* No comment provided by engineer. */ "Desktop devices" = "Számítógépek"; @@ -1935,7 +1936,7 @@ swipe action */ "Do not use credentials with proxy." = "Ne használja a hitelesítési adatokat proxyval."; /* No comment provided by engineer. */ -"Do NOT use private routing." = "NE használjon privát útválasztást."; +"Do NOT use private routing." = "NE legyen használva privát útválasztás."; /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "NE használja a SimpleXet segélyhívásokhoz."; @@ -1947,13 +1948,13 @@ swipe action */ "Don't create address" = "Ne hozzon létre címet"; /* No comment provided by engineer. */ -"Don't enable" = "Ne engedélyezze"; +"Don't enable" = "Nem engedélyezem"; /* No comment provided by engineer. */ "Don't miss important messages." = "Ne maradjon le a fontos üzenetekről."; /* alert action */ -"Don't show again" = "Ne mutasd újra"; +"Don't show again" = "Ne jelenjen meg újra"; /* No comment provided by engineer. */ "Done" = "Kész"; @@ -2002,10 +2003,10 @@ chat item action */ "Duration" = "Időtartam"; /* No comment provided by engineer. */ -"e2e encrypted" = "e2e titkosított"; +"e2e encrypted" = "végpontok között titkosított"; /* No comment provided by engineer. */ -"E2E encrypted notifications." = "Végpontok közötti titkosított értesítések."; +"E2E encrypted notifications." = "Végpontok között titkosított értesítések."; /* chat item action */ "Edit" = "Szerkesztés"; @@ -2080,7 +2081,7 @@ chat item action */ "enabled for you" = "engedélyezve az Ön számára"; /* No comment provided by engineer. */ -"Encrypt" = "Titkosít"; +"Encrypt" = "Titkosítás"; /* No comment provided by engineer. */ "Encrypt database?" = "Titkosítja az adatbázist?"; @@ -2149,10 +2150,10 @@ chat item action */ "Encryption renegotiation in progress." = "A titkosítás újraegyeztetése folyamatban van."; /* No comment provided by engineer. */ -"ended" = "befejeződött"; +"ended" = "hívás vége"; /* call status */ -"ended call %@" = "%@ hívása befejeződött"; +"ended call %@" = "%@ hívása véget ért"; /* No comment provided by engineer. */ "Enter correct passphrase." = "Adja meg a helyes jelmondatot."; @@ -2431,7 +2432,7 @@ chat item action */ "Error uploading the archive" = "Hiba történt az archívum feltöltésekor"; /* No comment provided by engineer. */ -"Error verifying passphrase:" = "Hiba történt a jelmondat hitelesítésekor:"; +"Error verifying passphrase:" = "Hiba történt a jelmondat ellenőrzésekor:"; /* No comment provided by engineer. */ "Error: " = "Hiba: "; @@ -2832,7 +2833,7 @@ snd error text */ "How SimpleX works" = "Hogyan működik a SimpleX"; /* No comment provided by engineer. */ -"How to" = "Hogyan"; +"How to" = "Útmutató"; /* No comment provided by engineer. */ "How to use it" = "Használati útmutató"; @@ -2856,7 +2857,7 @@ snd error text */ "If you enter your self-destruct passcode while opening the app:" = "Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot:"; /* No comment provided by engineer. */ -"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ha most kell használnia a csevegést, koppintson alább a **Befejezés később** lehetőségre (az alkalmazás újraindításakor fel lesz ajánlva az adatbázis átköltöztetése)."; +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ha most kell használnia a csevegést, koppintson lentebb a **Befejezés később** beállításra (az alkalmazás újraindításakor fel lesz ajánlva az adatbázis átköltöztetése)."; /* No comment provided by engineer. */ "Ignore" = "Mellőzés"; @@ -2979,7 +2980,7 @@ snd error text */ "Instant" = "Azonnali"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések el lesznek rejtve!\n"; +"Instant push notifications will be hidden!\n" = "Az azonnali leküldéses értesítések el lesznek rejtve!\n"; /* No comment provided by engineer. */ "Interface" = "Kezelőfelület"; @@ -3072,10 +3073,10 @@ snd error text */ "invited via your group link" = "meghíva a saját csoporthivatkozásán keresztül"; /* No comment provided by engineer. */ -"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a push-értesítések fogadását."; +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a leküldéses értesítések fogadását."; /* No comment provided by engineer. */ -"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi a push-értesítések fogadását."; +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi a leküldéses értesítések fogadását."; /* No comment provided by engineer. */ "IP address" = "IP-cím"; @@ -3141,7 +3142,7 @@ snd error text */ "Keep conversation" = "Beszélgetés megtartása"; /* No comment provided by engineer. */ -"Keep the app open to use it from desktop" = "A számítógépről való használathoz tartsd nyitva az alkalmazást"; +"Keep the app open to use it from desktop" = "Alkalmazás megnyitva tartása a számítógépről való használathoz"; /* alert title */ "Keep unused invitation?" = "Megtartja a fel nem használt meghívót?"; @@ -3195,7 +3196,7 @@ snd error text */ "Limitations" = "Korlátozások"; /* No comment provided by engineer. */ -"Link mobile and desktop apps! 🔗" = "Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗"; +"Link mobile and desktop apps! 🔗" = "Társítsa össze a hordozható eszköz- és a számítógépes alkalmazásokat! 🔗"; /* No comment provided by engineer. */ "Linked desktop options" = "Társított számítógép beállítások"; @@ -3252,7 +3253,7 @@ snd error text */ "Mark read" = "Megjelölés olvasottként"; /* No comment provided by engineer. */ -"Mark verified" = "Hitelesítés"; +"Mark verified" = "Megjelölés ellenőrzöttként"; /* No comment provided by engineer. */ "Markdown in messages" = "Markdown az üzenetekben"; @@ -3261,7 +3262,7 @@ snd error text */ "marked deleted" = "törlésre jelölve"; /* No comment provided by engineer. */ -"Max 30 seconds, received instantly." = "Max. 30 másodperc, azonnal érkezett."; +"Max 30 seconds, received instantly." = "Legfeljebb 30 másodperc, azonnal megérkezik."; /* No comment provided by engineer. */ "Media & file servers" = "Fájl- és médiakiszolgálók"; @@ -3308,10 +3309,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza!"; /* alert message */ @@ -3360,7 +3361,7 @@ snd error text */ "Message delivery warning" = "Üzenetkézbesítési figyelmeztetés"; /* No comment provided by engineer. */ -"Message draft" = "Üzenetvázlat"; +"Message draft" = "Piszkozatok"; /* item status text */ "Message forwarded" = "Továbbított üzenet"; @@ -3432,7 +3433,7 @@ snd error text */ "Messages sent" = "Elküldött üzenetek"; /* alert message */ -"Messages were deleted after you selected them." = "Az üzeneteket törölték miután kijelölte őket."; +"Messages were deleted after you selected them." = "Az üzeneteket törölték miután kiváasztotta őket."; /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve."; @@ -3468,7 +3469,7 @@ snd error text */ "Migration error:" = "Átköltöztetési hiba:"; /* No comment provided by engineer. */ -"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Sikertelen átköltöztetés. Koppintson a **Kihagyás** lehetőségre a jelenlegi adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat)."; +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Sikertelen átköltöztetés. Koppintson a **Kihagyás** beállításra a jelenlegi adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat)."; /* No comment provided by engineer. */ "Migration is completed" = "Az átköltöztetés befejeződött"; @@ -3573,10 +3574,10 @@ snd error text */ "New contact request" = "Új partneri kapcsolatkérés"; /* notification */ -"New contact:" = "Új kapcsolat:"; +"New contact:" = "Új partner:"; /* No comment provided by engineer. */ -"New desktop app!" = "Új számítógép-alkalmazás!"; +"New desktop app!" = "Új számítógépes alkalmazás!"; /* No comment provided by engineer. */ "New display name" = "Új megjelenítendő név"; @@ -3642,7 +3643,7 @@ snd error text */ "No chats with members" = "Nincsenek csevegések a tagokkal"; /* No comment provided by engineer. */ -"No contacts selected" = "Nincs partner kijelölve"; +"No contacts selected" = "Nincs partner kiválasztva"; /* No comment provided by engineer. */ "No contacts to add" = "Nincs hozzáadandó partner"; @@ -3657,7 +3658,7 @@ snd error text */ "No direct connection yet, message is forwarded by admin." = "Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja."; /* No comment provided by engineer. */ -"no e2e encryption" = "nincs e2e titkosítás"; +"no e2e encryption" = "nincs végpontok közötti titkosítás"; /* No comment provided by engineer. */ "No filtered chats" = "Nincsenek szűrt csevegések"; @@ -3696,7 +3697,7 @@ snd error text */ "No private routing session" = "Nincs privát útválasztási munkamenet"; /* No comment provided by engineer. */ -"No push server" = "Helyi"; +"No push server" = "Nincs kiszolgáló a leküldéses értesítésekhez"; /* No comment provided by engineer. */ "No received or sent files" = "Nincsenek fogadott vagy küldött fájlok"; @@ -3714,7 +3715,7 @@ snd error text */ "No servers to send files." = "Nincsenek fájlküldési kiszolgálók."; /* No comment provided by engineer. */ -"no subscription" = "nincs előfizetés"; +"no subscription" = "nincs feliratkozás"; /* copied message info in history */ "no text" = "nincs szöveg"; @@ -3738,7 +3739,7 @@ snd error text */ "Notes" = "Jegyzetek"; /* No comment provided by engineer. */ -"Nothing selected" = "Nincs semmi kijelölve"; +"Nothing selected" = "Nincs semmi kiválasztva"; /* alert title */ "Nothing to forward!" = "Nincs mit továbbítani!"; @@ -3833,37 +3834,37 @@ new chat action */ "Only you can add message reactions." = "Csak Ön adhat hozzá reakciókat az üzenetekhez."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra)"; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Csak Ön törölheti véglegesen az üzeneteket (partnere csak törlésre jelölheti meg azokat ). (24 óra)"; /* No comment provided by engineer. */ -"Only you can make calls." = "Csak Ön tud hívásokat indítani."; +"Only you can make calls." = "Csak Ön kezdeményezhet hívásokat."; /* No comment provided by engineer. */ -"Only you can send disappearing messages." = "Csak Ön tud eltűnő üzeneteket küldeni."; +"Only you can send disappearing messages." = "Csak Ön küldhet eltűnő üzeneteket."; /* No comment provided by engineer. */ "Only you can send files and media." = "Csak Ön küldhet fájlokat és médiatartalmakat."; /* No comment provided by engineer. */ -"Only you can send voice messages." = "Csak Ön tud hangüzeneteket küldeni."; +"Only you can send voice messages." = "Csak Ön küldhet hangüzeneteket."; /* No comment provided by engineer. */ "Only your contact can add message reactions." = "Csak a partnere adhat hozzá reakciókat az üzenetekhez."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Csak a partnere tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra)"; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Csak a partnere törölheti véglegesen az üzeneteket (Ön csak törlésre jelölheti meg azokat). (24 óra)"; /* No comment provided by engineer. */ -"Only your contact can make calls." = "Csak a partnere tud hívást indítani."; +"Only your contact can make calls." = "Csak a partnere kezdeményezhet hívásokat."; /* No comment provided by engineer. */ -"Only your contact can send disappearing messages." = "Csak a partnere tud eltűnő üzeneteket küldeni."; +"Only your contact can send disappearing messages." = "Csak a partnere küldhet eltűnő üzeneteket."; /* No comment provided by engineer. */ "Only your contact can send files and media." = "Csak a partnere küldhet fájlokat és médiatartalmakat."; /* No comment provided by engineer. */ -"Only your contact can send voice messages." = "Csak a partnere tud hangüzeneteket küldeni."; +"Only your contact can send voice messages." = "Csak a partnere küldhet hangüzeneteket."; /* alert action */ "Open" = "Megnyitás"; @@ -4070,7 +4071,7 @@ new chat action */ "Please report it to the developers." = "Jelentse a fejlesztőknek."; /* No comment provided by engineer. */ -"Please restart the app and migrate the database to enable push notifications." = "Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges push-értesítések engedélyezéséhez."; +"Please restart the app and migrate the database to enable push notifications." = "Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges leküldéses értesítések engedélyezéséhez."; /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez."; @@ -4181,7 +4182,7 @@ new chat action */ "Prohibit messages reactions." = "A reakciók hozzáadása az üzenetekhez le van tiltva."; /* No comment provided by engineer. */ -"Prohibit reporting messages to moderators." = "Az üzenetek a moderátorok felé történő jelentésének megtiltása."; +"Prohibit reporting messages to moderators." = "Az üzenetek jelentése a moderátorok felé le van tiltva."; /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között le van tiltva."; @@ -4229,10 +4230,10 @@ new chat action */ "Proxy requires password" = "A proxy jelszót igényel"; /* No comment provided by engineer. */ -"Push notifications" = "Push-értesítések"; +"Push notifications" = "Leküldéses értesítések"; /* No comment provided by engineer. */ -"Push server" = "Push-kiszolgáló"; +"Push server" = "Leküldéses értesítéskiszolgáló"; /* chat item text */ "quantum resistant e2e encryption" = "végpontok közötti kvantumbiztos titkosítás"; @@ -4241,13 +4242,13 @@ new chat action */ "Quantum resistant encryption" = "Kvantumbiztos titkosítás"; /* No comment provided by engineer. */ -"Rate the app" = "Értékelje az alkalmazást"; +"Rate the app" = "Alkalmazás értékelése"; /* No comment provided by engineer. */ "Reachable chat toolbar" = "Könnyen elérhető csevegési eszköztár"; /* chat item menu */ -"React…" = "Reagálj…"; +"React…" = "Reagálás…"; /* swipe action */ "Read" = "Olvasott"; @@ -4274,7 +4275,7 @@ new chat action */ "Receive errors" = "Üzenetfogadási hibák"; /* No comment provided by engineer. */ -"received answer…" = "válasz fogadása…"; +"received answer…" = "válasz érkezett…"; /* No comment provided by engineer. */ "Received at" = "Fogadva"; @@ -4283,7 +4284,7 @@ new chat action */ "Received at: %@" = "Fogadva: %@"; /* No comment provided by engineer. */ -"received confirmation…" = "visszaigazolás fogadása…"; +"received confirmation…" = "visszaigazolás érkezett…"; /* message info title */ "Received message" = "Fogadott üzenetbuborék színe"; @@ -4360,7 +4361,7 @@ swipe action */ "Reject" = "Elutasítás"; /* No comment provided by engineer. */ -"Reject (sender NOT notified)" = "Elutasítás (a kérés küldője NEM fog értesítést kapni)"; +"Reject (sender NOT notified)" = "Elutasítás (a kérés küldője NEM lesz értesítve)"; /* alert title */ "Reject contact request" = "Partneri kapcsolatkérés elutasítása"; @@ -4380,7 +4381,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Eltávolítás"; /* No comment provided by engineer. */ @@ -4395,7 +4396,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Eltávolítás"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Eltávolítja a tagot?"; /* No comment provided by engineer. */ @@ -4501,7 +4502,7 @@ swipe action */ "Reset all hints" = "Tippek visszaállítása"; /* No comment provided by engineer. */ -"Reset all statistics" = "Az összes statisztika visszaállítása"; +"Reset all statistics" = "Összes statisztika visszaállítása"; /* No comment provided by engineer. */ "Reset all statistics?" = "Visszaállítja az összes statisztikát?"; @@ -4712,7 +4713,7 @@ chat item action */ "Secured" = "Biztosítva"; /* No comment provided by engineer. */ -"Security assessment" = "Biztonsági kiértékelés"; +"Security assessment" = "Biztonsági felmérés"; /* No comment provided by engineer. */ "Security code" = "Biztonsági kód"; @@ -4721,16 +4722,16 @@ chat item action */ "security code changed" = "biztonsági kódja módosult"; /* chat item action */ -"Select" = "Kijelölés"; +"Select" = "Kiválasztás"; /* No comment provided by engineer. */ -"Select chat profile" = "Csevegési profil kijelölése"; +"Select chat profile" = "Csevegési profil kiválasztása"; /* No comment provided by engineer. */ -"Selected %lld" = "%lld kijelölve"; +"Selected %lld" = "%lld kiválasztva"; /* No comment provided by engineer. */ -"Selected chat preferences prohibit this message." = "A kijelölt csevegési beállítások tiltják ezt az üzenetet."; +"Selected chat preferences prohibit this message." = "A kiválasztott csevegési beállítások tiltják ezt az üzenetet."; /* No comment provided by engineer. */ "Self-destruct" = "Önmegsemmisítés"; @@ -4823,13 +4824,13 @@ chat item action */ "Sending file will be stopped." = "A fájl küldése le fog állni."; /* No comment provided by engineer. */ -"Sending receipts is disabled for %lld contacts" = "A kézbesítési jelentések le vannak tiltva %lld partnernél"; +"Sending receipts is disabled for %lld contacts" = "A kézbesítési jelentések le vannak tiltva %lld partner számára"; /* No comment provided by engineer. */ "Sending receipts is disabled for %lld groups" = "A kézbesítési jelentések le vannak tiltva %lld csoportban"; /* No comment provided by engineer. */ -"Sending receipts is enabled for %lld contacts" = "A kézbesítési jelentések engedélyezve vannak %lld partnernél"; +"Sending receipts is enabled for %lld contacts" = "A kézbesítési jelentések engedélyezve vannak %lld partner számára"; /* No comment provided by engineer. */ "Sending receipts is enabled for %lld groups" = "A kézbesítési jelentések engedélyezve vannak %lld csoportban"; @@ -4919,7 +4920,7 @@ chat item action */ "Servers statistics will be reset - this cannot be undone!" = "A kiszolgálók statisztikái visszaállnak – ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ -"Session code" = "Munkamenet kód"; +"Session code" = "Munkamenet kódja"; /* No comment provided by engineer. */ "Set 1 day" = "Beállítva 1 nap"; @@ -4961,7 +4962,7 @@ chat item action */ "Set passphrase to export" = "Jelmondat beállítása az exportáláshoz"; /* No comment provided by engineer. */ -"Set profile bio and welcome message." = "Névjegy és üdvözlőüzenet beállítása a profilokhoz."; +"Set profile bio and welcome message." = "Életrajz és üdvözlőüzenet beállítása a profilokhoz."; /* No comment provided by engineer. */ "Set the message shown to new members!" = "Megjelenítendő üzenet beállítása az új tagok számára!"; @@ -5079,7 +5080,7 @@ chat item action */ "SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó?"; /* alert title */ -"SimpleX address settings" = "Beállítások automatikus elfogadása"; +"SimpleX address settings" = "SimpleX-címbeállítások"; /* simplex link type */ "SimpleX channel link" = "SimpleX-csatornahivatkozás"; @@ -5142,7 +5143,7 @@ chat item action */ "Skipped messages" = "Kihagyott üzenetek"; /* No comment provided by engineer. */ -"Small groups (max 20)" = "Kis csoportok (max. 20 tag)"; +"Small groups (max 20)" = "Kis csoportok (legfeljebb 20 tag)"; /* No comment provided by engineer. */ "SMP server" = "SMP-kiszolgáló"; @@ -5182,7 +5183,7 @@ report reason */ "standard end-to-end encryption" = "szabványos végpontok közötti titkosítás"; /* No comment provided by engineer. */ -"Start chat" = "Csevegés indítása"; +"Start chat" = "Csevegés elindítása"; /* No comment provided by engineer. */ "Start chat?" = "Elindítja a csevegést?"; @@ -5194,7 +5195,7 @@ report reason */ "Starting from %@." = "Statisztikagyűjtés kezdete: %@."; /* No comment provided by engineer. */ -"starting…" = "indítás…"; +"starting…" = "hívás indítása…"; /* No comment provided by engineer. */ "Statistics" = "Statisztikák"; @@ -5425,10 +5426,10 @@ report reason */ "The second preset operator in the app!" = "A második előre beállított üzemeltető az alkalmazásban!"; /* No comment provided by engineer. */ -"The second tick we missed! ✅" = "A második jelölés, amit kihagytunk! ✅"; +"The second tick we missed! ✅" = "A második pipa, ami már nagyon hiányzott! ✅"; /* alert message */ -"The sender will NOT be notified" = "A kérés küldője NEM fog értesítést kapni"; +"The sender will NOT be notified" = "A kérés küldője NEM lesz értesítve"; /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "A jelenlegi **%@** nevű csevegési profiljához tartozó új kapcsolatok kiszolgálói."; @@ -5458,10 +5459,10 @@ report reason */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Ez a művelet nem vonható vissza – az összes fogadott és küldött fájl a médiatartalmakkal együtt törölve lesznek. Az alacsony felbontású képek viszont megmaradnak."; /* No comment provided by engineer. */ -"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Ez a művelet nem vonható vissza – a kijelöltnél korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet."; +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Ez a művelet nem vonható vissza – a kiválasztott üzenettől korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet."; /* alert message */ -"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Ez a művelet nem vonható vissza – a kijelölt üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből."; +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Ez a művelet nem vonható vissza – a kiválasztott üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből."; /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek."; @@ -5557,7 +5558,7 @@ report reason */ "To send commands you must be connected." = "A parancsok küldéséhez kapcsolódva kell lennie."; /* No comment provided by engineer. */ -"To support instant push notifications the chat database has to be migrated." = "Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges."; +"To support instant push notifications the chat database has to be migrated." = "Az azonnali leküldéses értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges."; /* alert message */ "To use another profile after connection attempt, delete the chat and use the link again." = "Másik profil használatához a kapcsolatfelvételi kísérlet után törölje a csevegést, és használja újra a hivatkozást."; @@ -5566,7 +5567,7 @@ report reason */ "To use the servers of **%@**, accept conditions of use." = "A(z) **%@** kiszolgálóinak használatához fogadja el a használati feltételeket."; /* No comment provided by engineer. */ -"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal."; +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal."; /* No comment provided by engineer. */ "Toggle chat list:" = "Csevegési lista ki/be:"; @@ -5701,7 +5702,7 @@ report reason */ "Update" = "Frissítés"; /* No comment provided by engineer. */ -"Update database passphrase" = "Az adatbázis jelmondatának módosítása"; +"Update database passphrase" = "Adatbázis jelmondatának módosítása"; /* No comment provided by engineer. */ "Update network settings?" = "Módosítja a hálózati beállításokat?"; @@ -5830,7 +5831,7 @@ report reason */ "Use web port" = "Webport használata"; /* No comment provided by engineer. */ -"User selection" = "Felhasználó kijelölése"; +"User selection" = "Felhasználó kiválasztása"; /* No comment provided by engineer. */ "Username" = "Felhasználónév"; @@ -5845,25 +5846,25 @@ report reason */ "v%@ (%@)" = "v%@ (%@)"; /* No comment provided by engineer. */ -"Verify code with desktop" = "Kód hitelesítése a számítógépen"; +"Verify code with desktop" = "Kód ellenőrzése a számítógépen"; /* No comment provided by engineer. */ -"Verify connection" = "Kapcsolat hitelesítése"; +"Verify connection" = "Kapcsolat ellenőrzése"; /* No comment provided by engineer. */ -"Verify connection security" = "Biztonságos kapcsolat hitelesítése"; +"Verify connection security" = "Biztonságos kapcsolat ellenőrzése"; /* No comment provided by engineer. */ -"Verify connections" = "Kapcsolatok hitelesítése"; +"Verify connections" = "Kapcsolatok ellenőrzése"; /* No comment provided by engineer. */ -"Verify database passphrase" = "Az adatbázis jelmondatának hitelesítése"; +"Verify database passphrase" = "Adatbázis jelmondatának ellenőrzése"; /* No comment provided by engineer. */ -"Verify passphrase" = "Jelmondat hitelesítése"; +"Verify passphrase" = "Jelmondat ellenőrzése"; /* No comment provided by engineer. */ -"Verify security code" = "Biztonsági kód hitelesítése"; +"Verify security code" = "Biztonsági kód ellenőrzése"; /* No comment provided by engineer. */ "Via browser" = "Böngészőn keresztül"; @@ -5890,7 +5891,7 @@ report reason */ "Video call" = "Videóhívás"; /* No comment provided by engineer. */ -"video call (not e2e encrypted)" = "videóhívás (nem e2e titkosított)"; +"video call (not e2e encrypted)" = "videóhívás (végpontok között NEM titkosított)"; /* No comment provided by engineer. */ "Video will be received when your contact completes uploading it." = "A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését."; @@ -6091,7 +6092,7 @@ report reason */ "You are invited to group" = "Ön meghívást kapott a csoportba"; /* subscription status explanation */ -"You are not connected to the server used to receive messages from this connection (no subscription)." = "Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs előfizetés)."; +"You are not connected to the server used to receive messages from this connection (no subscription)." = "Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs feliratkozás)."; /* No comment provided by engineer. */ "You are not connected to these servers. Private routing is used to deliver messages to them." = "Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál."; @@ -6148,7 +6149,7 @@ report reason */ "You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt a SimpleX-címet a partnereivel, hogy kapcsolatba léphessenek vele: **%@**."; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el"; +"You can start chat via app Settings / Database or by restarting the app" = "A csevegés elindítható az alkalmazás „Beállítások / Adatbázis” menüjében vagy az alkalmazás újraindításával"; /* No comment provided by engineer. */ "You can still view conversation with %@ in the list of chats." = "A(z) %@ nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában."; @@ -6181,7 +6182,7 @@ report reason */ "you changed role of %@ to %@" = "Ön a következőre módosította %1$@ szerepkörét: „%2$@”"; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Nem sikerült hitelesíteni; próbálja meg újra."; +"You could not be verified; please try again." = "Nem sikerült ellenőrizni; próbálja meg újra."; /* No comment provided by engineer. */ "You decide who can connect." = "Ön dönti el, hogy kivel beszélget."; @@ -6262,10 +6263,10 @@ report reason */ "You will still receive calls and notifications from muted profiles when they are active." = "Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak."; /* No comment provided by engineer. */ -"You will stop receiving messages from this chat. Chat history will be preserved." = "Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak."; +"You will stop receiving messages from this chat. Chat history will be preserved." = "Nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak."; /* No comment provided by engineer. */ -"You will stop receiving messages from this group. Chat history will be preserved." = "Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak."; +"You will stop receiving messages from this group. Chat history will be preserved." = "Nem fog több üzenetet kapni ebből a csoportból, de a csevegés előzményei megmaradnak."; /* No comment provided by engineer. */ "You won't lose your contacts if you later delete your address." = "Nem veszíti el a partnereit, ha később törli a címét."; @@ -6307,13 +6308,13 @@ report reason */ "Your contact" = "Partner"; /* No comment provided by engineer. */ -"Your contact sent a file that is larger than currently supported maximum size (%@)." = "A partnere a jelenleg megengedett maximális méretű (%@) fájlnál nagyobbat küldött."; +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "A partnere a jelenleg támogatott legnagyobb (%@) fájlméretnél nagyobbat küldött."; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "A partnerei engedélyezhetik a teljes üzenet törlését."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "A partnerei továbbra is kapcsolódva maradnak."; +"Your contacts will remain connected." = "A partnereivel továbbra is kapcsolatban marad."; /* No comment provided by engineer. */ "Your credentials may be sent unencrypted." = "A hitelesítési adatai titkosítatlanul is elküldhetők."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 511a6835e5..9e2a27e618 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1733,7 +1733,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Eliminare il messaggio?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Elimina messaggi"; /* No comment provided by engineer. */ @@ -3308,10 +3309,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Il ruolo del membro verrà cambiato in \"%@\". Il membro riceverà un invito nuovo."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "Il membro verrà rimosso dalla chat, non è reversibile!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!"; /* alert message */ @@ -3899,7 +3900,7 @@ new chat action */ "Open new chat" = "Apri una chat nuova"; /* new chat action */ -"Open new group" = "Apri un gruppo nuovo"; +"Open new group" = "Apri il nuovo gruppo"; /* No comment provided by engineer. */ "Open Settings" = "Apri le impostazioni"; @@ -4380,7 +4381,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Il server relay protegge il tuo indirizzo IP, ma può osservare la durata della chiamata."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Rimuovi"; /* No comment provided by engineer. */ @@ -4395,7 +4396,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Rimuovi membro"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Rimuovere il membro?"; /* No comment provided by engineer. */ diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index d4510af72f..480eb39d36 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -374,9 +374,18 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledged" = "了承済み"; +/* No comment provided by engineer. */ +"Active connections" = "アクティブな接続"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。"; +/* No comment provided by engineer. */ +"Add friends" = "友達を追加"; + +/* No comment provided by engineer. */ +"Add list" = "リストを追加"; + /* No comment provided by engineer. */ "Add profile" = "プロフィールを追加"; @@ -386,9 +395,15 @@ swipe action */ /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "QRコードでサーバを追加する。"; +/* No comment provided by engineer. */ +"Add team members" = "チームメンバーを追加"; + /* No comment provided by engineer. */ "Add to another device" = "別の端末に追加"; +/* No comment provided by engineer. */ +"Add to list" = "リストに追加"; + /* No comment provided by engineer. */ "Add welcome message" = "ウェルカムメッセージを追加"; @@ -404,6 +419,9 @@ swipe action */ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "アドレス変更は中止されます。古い受信アドレスが使用されます。"; +/* No comment provided by engineer. */ +"Address settings" = "アドレス設定"; + /* member role */ "admin" = "管理者"; @@ -422,6 +440,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "暗号化に同意しています…"; +/* No comment provided by engineer. */ +"All" = "すべて"; + /* No comment provided by engineer. */ "All app data is deleted." = "すべてのアプリデータが削除されます。"; @@ -434,6 +455,9 @@ swipe action */ /* No comment provided by engineer. */ "All group members will remain connected." = "グループ全員の接続が継続します。"; +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "すべてのメッセージが削除されます。この操作は元に戻せません!"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ。"; @@ -455,9 +479,15 @@ swipe action */ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "連絡先が通話を許可している場合のみ通話を許可する。"; +/* No comment provided by engineer. */ +"Allow calls?" = "通話を許可しますか?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "連絡先が許可している場合のみ消えるメッセージを許可する。"; +/* No comment provided by engineer. */ +"Allow downgrade" = "ダウングレードを許可する"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "送信相手も永久メッセージ削除を許可する時のみに許可する。(24時間)"; @@ -530,6 +560,9 @@ swipe action */ /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "指定された名前の空のチャット プロファイルが作成され、アプリが通常どおり開きます。"; +/* report reason */ +"Another reason" = "他の理由"; + /* No comment provided by engineer. */ "Answer call" = "通話に応答"; @@ -569,9 +602,15 @@ swipe action */ /* No comment provided by engineer. */ "Apply to" = "に適用する"; +/* No comment provided by engineer. */ +"Archive" = "アーカイブ"; + /* No comment provided by engineer. */ "Archive and upload" = "アーカイブとアップロード"; +/* No comment provided by engineer. */ +"Archived contacts" = "アーカイブされた連絡先"; + /* No comment provided by engineer. */ "Attach" = "添付する"; @@ -668,6 +707,9 @@ swipe action */ /* No comment provided by engineer. */ "Calls" = "通話"; +/* alert title */ +"Can't change profile" = "プロフィールを変更できません"; + /* No comment provided by engineer. */ "Can't invite contact!" = "連絡先を招待できません!"; @@ -679,12 +721,18 @@ alert button new chat action */ "Cancel" = "中止"; +/* No comment provided by engineer. */ +"Cancel migration" = "移行を中止する"; + /* feature offered item */ "cancelled %@" = "キャンセルされました %@"; /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "データベースのパスワードを保存するためのキーチェーンにアクセスできません"; +/* No comment provided by engineer. */ +"Cannot forward message" = "メッセージを転送できません"; + /* alert title */ "Cannot receive file" = "ファイル受信ができません"; @@ -734,6 +782,9 @@ set passcode view */ /* chat item text */ "changing address…" = "アドレスを変更しています…"; +/* No comment provided by engineer. */ +"Chat" = "チャット"; + /* No comment provided by engineer. */ "Chat console" = "チャットのコンソール"; @@ -752,6 +803,9 @@ set passcode view */ /* No comment provided by engineer. */ "Chat is stopped" = "チャットが停止してます"; +/* No comment provided by engineer. */ +"Chat list" = "チャット一覧"; + /* No comment provided by engineer. */ "Chat preferences" = "チャット設定"; @@ -764,6 +818,9 @@ set passcode view */ /* No comment provided by engineer. */ "Chats" = "チャット"; +/* No comment provided by engineer. */ +"Check messages every 20 min." = "20分おきにメッセージを確認する。"; + /* alert title */ "Check server address and try again." = "サーバのアドレスを確認してから再度試してください。"; @@ -1168,7 +1225,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "メッセージを削除しますか?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "メッセージを削除"; /* No comment provided by engineer. */ @@ -2103,7 +2161,7 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "メンバーの役割が \"%@\" に変更されます。 メンバーは新たな招待を受け取ります。"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "メンバーをグループから除名する (※元に戻せません※)!"; /* No comment provided by engineer. */ @@ -2644,13 +2702,13 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "リレー サーバーは IP アドレスを保護しますが、通話時間は監視されます。"; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "削除"; /* No comment provided by engineer. */ "Remove member" = "メンバーを除名する"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "メンバーを除名しますか?"; /* No comment provided by engineer. */ diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 79e3da3b01..29f8bb5b3f 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1688,7 +1688,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Verwijder bericht?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Verwijder berichten"; /* No comment provided by engineer. */ @@ -3197,10 +3198,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "De rol van lid wordt gewijzigd in \"%@\". Het lid ontvangt een nieuwe uitnodiging."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!"; /* alert message */ @@ -4218,7 +4219,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Relay server beschermt uw IP-adres, maar kan de duur van het gesprek observeren."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Verwijderen"; /* No comment provided by engineer. */ @@ -4230,7 +4231,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Lid verwijderen"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Lid verwijderen?"; /* No comment provided by engineer. */ diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 34c79eeef4..9ef572364f 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -346,12 +346,21 @@ alert action swipe action */ "Accept" = "Akceptuj"; +/* alert action */ +"Accept as member" = "Zaakceptuj jako członka"; + +/* alert action */ +"Accept as observer" = "Zaakceptuj jako obserwatora"; + /* No comment provided by engineer. */ "Accept conditions" = "Zaakceptuj warunki"; /* No comment provided by engineer. */ "Accept connection request?" = "Zaakceptować prośbę o połączenie?"; +/* alert title */ +"Accept contact request" = "Zaakceptuj prośby o kontakt"; + /* notification body */ "Accept contact request from %@?" = "Zaakceptuj prośbę o kontakt od %@?"; @@ -359,6 +368,9 @@ swipe action */ swipe action */ "Accept incognito" = "Akceptuj incognito"; +/* alert title */ +"Accept member" = "Zaakceptuj członka"; + /* call status */ "accepted call" = "zaakceptowane połączenie"; @@ -386,6 +398,9 @@ swipe action */ /* No comment provided by engineer. */ "Add list" = "Dodaj listę"; +/* placeholder for sending contact request */ +"Add message" = "Dodaj wiadomość"; + /* No comment provided by engineer. */ "Add profile" = "Dodaj profil"; @@ -503,6 +518,9 @@ swipe action */ /* No comment provided by engineer. */ "All reports will be archived for you." = "Wszystkie raporty zostaną dla Ciebie zarchiwizowane."; +/* No comment provided by engineer. */ +"All servers" = "Wszystkie serwery"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Wszystkie Twoje kontakty pozostaną połączone."; @@ -527,6 +545,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow downgrade" = "Zezwól na obniżenie wersji"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Zezwalaj na pliki i media tylko wtedy, gdy Twój kontakt na to pozwala."; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny)"; @@ -578,6 +599,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Zezwól swoim kontaktom na wysyłanie znikających wiadomości."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Pozwól kontaktom wysyłać pliki i media."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Zezwól swoim kontaktom na wysyłanie wiadomości głosowych."; @@ -755,6 +779,9 @@ swipe action */ /* No comment provided by engineer. */ "Better groups" = "Lepsze grupy"; +/* No comment provided by engineer. */ +"Better groups performance" = "Lepsze działanie grup"; + /* No comment provided by engineer. */ "Better message dates." = "Lepsze daty wiadomości."; @@ -767,6 +794,9 @@ swipe action */ /* No comment provided by engineer. */ "Better notifications" = "Lepsze powiadomienia"; +/* No comment provided by engineer. */ +"Better privacy and security" = "Lepsza prywatność i bezpieczeństwo"; + /* No comment provided by engineer. */ "Better security ✅" = "Lepsze zabezpieczenia ✅"; @@ -816,6 +846,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "bold" = "pogrubiona"; +/* No comment provided by engineer. */ +"Bot" = "Bot"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości."; @@ -828,6 +861,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Zarówno Ty, jak i Twój kontakt możecie wysyłać znikające wiadomości."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Zarówno Ty, jak i Twój kontakt możecie wysyłać pliki i media."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe."; @@ -1559,7 +1595,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Usunąć wiadomość?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Usuń wiadomości"; /* No comment provided by engineer. */ @@ -2876,7 +2913,7 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Rola członka zostanie zmieniona na \"%@\". Członek otrzyma nowe zaproszenie."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Członek zostanie usunięty z grupy - nie można tego cofnąć!"; /* No comment provided by engineer. */ @@ -3711,7 +3748,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Serwer przekaźnikowy chroni Twój adres IP, ale może obserwować czas trwania połączenia."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Usuń"; /* No comment provided by engineer. */ @@ -3723,7 +3760,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Usuń członka"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Usunąć członka?"; /* No comment provided by engineer. */ diff --git a/apps/ios/product/README.md b/apps/ios/product/README.md new file mode 100644 index 0000000000..107c0e6569 --- /dev/null +++ b/apps/ios/product/README.md @@ -0,0 +1,258 @@ +# SimpleX Chat iOS -- Product Overview + +> SimpleX Chat iOS product specification. Bidirectional code links: product docs reference source files, source files reference product docs. +> +> **Related spec:** [spec/README.md](../spec/README.md) | [spec/architecture.md](../spec/architecture.md) + +## Table of Contents + +1. [Vision](#vision) +2. [Target Users](#target-users) +3. [Capability Map](#capability-map) +4. [Navigation Map](#navigation-map) +5. [Related Specifications](#related-specifications) + +## Executive Summary + +SimpleX Chat is the first messaging platform with no user identifiers of any kind -- not even random numbers. It provides end-to-end encrypted messaging (with optional post-quantum cryptography), audio/video calls, file sharing, and group communication through a fully decentralized architecture where users control their own SMP relay servers. The iOS app is a native SwiftUI application backed by a Haskell core library. + +--- + +## Vision + +SimpleX Chat is the first messaging platform that has no user identifiers -- not even random numbers. It uses double-ratchet end-to-end encryption with optional post-quantum cryptography. The system is fully decentralized with user-controlled SMP relay servers. + +The protocol design ensures that no server or network observer can determine who communicates with whom. Each conversation uses separate unidirectional messaging queues on potentially different servers, and there is no shared identifier between the sender and receiver queues. + +--- + +## Target Users + +- **Privacy-conscious individuals** wanting secure messaging without phone-number or email-based identity +- **Groups and communities** needing encrypted group communication with role-based access control +- **Users avoiding identity linkage** who want to communicate without any persistent user identifier +- **Organizations** needing self-hosted messaging infrastructure with full control over relay servers + +--- + +## Capability Map + +### 1. Messaging + +Core message composition, delivery, and interaction features. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| Text with markdown | Rich text formatting with SimpleX markdown syntax | `Shared/Views/Chat/ComposeMessage/ComposeView.swift` | +| Images | Compressed inline images (up to 255KB) | `Shared/Views/Chat/ChatItem/CIImageView.swift` | +| Video | Video message recording and playback | `Shared/Views/Chat/ChatItem/CIVideoView.swift` | +| Voice messages | Audio recording and playback (5min / 510KB limit) | `Shared/Views/Chat/ChatItem/CIVoiceView.swift` | +| File sharing | Files up to 1GB via XFTP protocol | `Shared/Views/Chat/ChatItem/CIFileView.swift` | +| Link previews | OpenGraph metadata extraction and display | `Shared/Views/Chat/ChatItem/CILinkView.swift` | +| Message reactions | Emoji reactions on sent/received messages | `Shared/Views/Chat/ChatItem/EmojiItemView.swift` | +| Message editing | Edit previously sent messages | `Shared/Views/Chat/ComposeMessage/ComposeView.swift` | +| Message deletion | Broadcast delete (for recipient) or internal-only delete | `Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift` | +| Timed messages | Self-destructing messages with configurable TTL | `Shared/Views/Chat/ChatItem/CIChatFeatureView.swift` | +| Quoted replies | Reply to specific messages with quote context | `Shared/Views/Chat/ComposeMessage/ContextItemView.swift` | +| Forwarding | Forward messages between chats | `Shared/Views/Chat/ChatItemForwardingView.swift` | +| Search | Full-text search within conversations | `Shared/Views/Chat/ChatView.swift` | +| Message reports | Report messages to group moderators | `Shared/Views/Chat/ChatView.swift` | + +### 2. Contacts + +Establishing, managing, and verifying contacts. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| Add via SimpleX address | Connect using a SimpleX contact address | `Shared/Views/NewChat/NewChatView.swift` | +| Add via QR code | Scan QR code to establish connection | `Shared/Views/Chat/ScanCodeView.swift` | +| Contact requests | Accept or reject incoming contact requests | `Shared/Views/ChatList/ContactRequestView.swift` | +| Local aliases | Set private display names for contacts | `Shared/Views/Chat/ChatInfoView.swift` | +| Contact verification | Compare security codes out-of-band | `Shared/Views/Chat/VerifyCodeView.swift` | +| Blocking | Block contacts from sending messages | `Shared/Views/Chat/ChatInfoView.swift` | +| Incognito mode | Per-contact random profile generation | `Shared/Views/UserSettings/IncognitoHelp.swift` | +| Bot detection | Identify automated/bot contacts | `SimpleXChat/ChatTypes.swift` | + +### 3. Groups + +Multi-party encrypted conversations with role-based management. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| Create groups | Create new group with initial members | `Shared/Views/NewChat/AddGroupView.swift` | +| Invite members | Invite by individual contact or link | `Shared/Views/Chat/Group/AddGroupMembersView.swift` | +| Member roles | Owner, admin, moderator, member, observer | `SimpleXChat/ChatTypes.swift` | +| Member admission | Queue-based admission with review workflow | `Shared/Views/Chat/Group/MemberAdmissionView.swift` | +| Group links | Shareable invite links for groups | `Shared/Views/Chat/Group/GroupLinkView.swift` | +| Business chat mode | Structured business communication groups | `Shared/Views/Chat/Group/GroupChatInfoView.swift` | +| Content moderation | Member reports and moderator actions | `Shared/Views/Chat/Group/MemberSupportView.swift` | +| Group preferences | Configure group-level feature settings | `Shared/Views/Chat/Group/GroupPreferencesView.swift` | +| Member direct contacts | Establish direct chats from group membership | `Shared/Views/Chat/Group/GroupMemberInfoView.swift` | + +### 4. Calling + +End-to-end encrypted audio and video communication. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| E2E encrypted calls | Audio/video calls via WebRTC with E2E encryption | `Shared/Views/Call/WebRTCClient.swift` | +| CallKit integration | Native iOS system call UI (ring, answer, decline) | `Shared/Views/Call/CallController.swift` | +| Audio device switching | Switch between speaker, earpiece, Bluetooth | `Shared/Views/Call/CallAudioDeviceManager.swift` | +| Call history | Call events displayed as chat items | `Shared/Views/Chat/ChatItem/CICallItemView.swift` | +| Incoming call view | Dedicated UI for incoming call notifications | `Shared/Views/Call/IncomingCallView.swift` | + +### 5. Privacy & Security + +Encryption, authentication, and privacy controls. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| E2E encryption | Double-ratchet encryption for all messages | `SimpleXChat/API.swift` | +| Post-quantum encryption | Optional PQ key exchange for direct chats | `SimpleXChat/ChatTypes.swift` | +| Local authentication | Face ID, Touch ID, or app passcode lock | `Shared/Views/LocalAuth/LocalAuthView.swift` | +| Hidden profiles | Password-protected profiles invisible in UI | `Shared/Views/UserSettings/HiddenProfileView.swift` | +| Database encryption | AES encryption of local SQLite database | `Shared/Views/Database/DatabaseEncryptionView.swift` | +| Screen privacy | Blur app content when in app switcher | `Shared/Views/UserSettings/PrivacySettings.swift` | +| Encrypted file storage | Local files encrypted at rest | `SimpleXChat/CryptoFile.swift` | +| Delivery receipts control | Toggle delivery/read receipts per contact/group | `Shared/Views/UserSettings/SetDeliveryReceiptsView.swift` | + +### 6. User Management + +Multiple profiles and identity management. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| Multiple profiles | Multiple user profiles within one app | `Shared/Views/UserSettings/UserProfilesView.swift` | +| Active user switching | Switch between profiles via user picker | `Shared/Views/ChatList/UserPicker.swift` | +| Incognito contacts | Per-contact random identities | `Shared/Views/UserSettings/IncognitoHelp.swift` | +| Profile sharing | Share profile via contact address link | `Shared/Views/UserSettings/UserAddressView.swift` | +| User muting | Mute notifications for specific profiles | `Shared/Views/ChatList/UserPicker.swift` | + +### 7. Network + +Server configuration, proxy support, and connectivity. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| Custom SMP servers | Configure personal SMP relay servers | `Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift` | +| Custom XFTP servers | Configure personal XFTP file servers | `Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift` | +| Tor/onion support | Route traffic through Tor .onion addresses | `Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift` | +| SOCKS5 proxy | Route connections through SOCKS5 proxy | `Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift` | +| Custom ICE servers | Configure WebRTC ICE/TURN servers | `Shared/Views/UserSettings/RTCServers.swift` | +| Network timeouts | Configure connection timeout parameters | `Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift` | + +### 8. Customization + +Visual appearance and UI preferences. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| Themes | Light, dark, SimpleX, black, and custom themes | `Shared/Theme/ThemeManager.swift` | +| Wallpapers | Preset and custom chat wallpapers | `Shared/Views/Helpers/ChatWallpaper.swift` | +| Chat bubble styling | Customize message bubble appearance | `SimpleXChat/Theme/ThemeTypes.swift` | +| One-handed UI mode | Compact layout for single-hand use | `Shared/Views/ChatList/OneHandUICard.swift` | +| Language selection | In-app language override | `Shared/Views/UserSettings/AppearanceSettings.swift` | + +### 9. Data Management + +Import, export, encryption, and storage management. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| Export/import profiles | Full database export and import | `Shared/Views/Database/DatabaseView.swift` | +| Database encryption | Encrypt/decrypt local database with passphrase | `Shared/Views/Database/DatabaseEncryptionView.swift` | +| Local file encryption | Encrypt stored media and attachments | `SimpleXChat/CryptoFile.swift` | +| Storage breakdown | View storage usage by category | `Shared/Views/UserSettings/StorageView.swift` | +| Device-to-device migration | Migrate full profile between iOS devices | `Shared/Views/Migration/MigrateFromDevice.swift` | + +### 10. Desktop Integration + +Remote control of the mobile app from a desktop client. + +| Feature | Description | Key Source (Swift) | +|---------|-------------|--------------------| +| Remote control pairing | Pair with desktop app via QR code | `Shared/Views/RemoteAccess/ConnectDesktopView.swift` | +| Session management | Manage active desktop control sessions | `Shared/Views/RemoteAccess/ConnectDesktopView.swift` | + +--- + +## Navigation Map + +``` +Onboarding + OnboardingView.swift + -> SimpleXInfo -> CreateProfile -> ChooseServerOperators -> SetNotificationsMode -> CreateSimpleXAddress + -> ChatListView (home) + +ChatListView (home) + Shared/Views/ChatList/ChatListView.swift + -> ChatView .................. (tap conversation row) + -> NewChatMenuButton ......... (+ button) + -> SettingsView .............. (gear icon) + -> UserPicker ................ (avatar tap) + -> TagListView ............... (tag filter bar) + -> ServersSummaryView ........ (server status) + +ChatView + Shared/Views/Chat/ChatView.swift + -> ChatInfoView .............. (contact name tap, direct chat) + -> GroupChatInfoView ......... (group name tap, group chat) + -> ActiveCallView ............ (call button) + -> ComposeView ............... (message input area) + -> ChatItemInfoView .......... (long press -> info) + -> ChatItemForwardingView .... (long press -> forward) + -> SecondaryChatView ......... (member support thread) + +ChatInfoView + Shared/Views/Chat/ChatInfoView.swift + -> ContactPreferencesView .... (preferences) + -> VerifyCodeView ............ (verify security code) + +GroupChatInfoView + Shared/Views/Chat/Group/GroupChatInfoView.swift + -> GroupProfileView .......... (edit profile) + -> AddGroupMembersView ....... (invite members) + -> GroupLinkView ............. (manage group link) + -> MemberAdmissionView ....... (admission settings) + -> GroupPreferencesView ...... (group feature settings) + -> GroupMemberInfoView ....... (tap member) + -> GroupWelcomeView .......... (welcome message) + +NewChatMenuButton + Shared/Views/NewChat/NewChatMenuButton.swift + -> NewChatView ............... (QR scanner / paste link) + -> AddGroupView .............. (create group) + -> UserAddressView ........... (create SimpleX address) + +SettingsView + Shared/Views/UserSettings/SettingsView.swift + -> AppearanceSettings ........ (themes, wallpapers, UI) + -> NetworkAndServers ......... (SMP/XFTP/proxy config) + -> PrivacySettings ........... (privacy toggles) + -> NotificationsView ......... (push notification mode) + -> DatabaseView .............. (export/import/encrypt) + -> CallSettings .............. (call preferences) + -> StorageView ............... (storage usage) + -> VersionView ............... (about/version) + -> DeveloperView ............. (developer options) + +UserPicker + Shared/Views/ChatList/UserPicker.swift + -> UserProfilesView .......... (manage all profiles) + -> UserAddressView ........... (SimpleX address) + -> PreferencesView ........... (user preferences) + -> SettingsView .............. (app settings) + -> ConnectDesktopView ........ (pair with desktop) +``` + +--- + +## Related Specifications + +- [concepts.md](concepts.md) -- Feature concept index with bidirectional code links +- [glossary.md](glossary.md) -- Domain term glossary +- [spec/README.md](../spec/README.md) -- Technical specification overview +- [spec/architecture.md](../spec/architecture.md) -- Architecture specification +- Haskell core: `../../src/Simplex/Chat/Controller.hs`, `../../src/Simplex/Chat/Types.hs` +- Swift model: `Shared/Model/ChatModel.swift`, `SimpleXChat/ChatTypes.swift` +- Swift API bridge: `SimpleXChat/API.swift`, `Shared/Model/SimpleXAPI.swift` diff --git a/apps/ios/product/concepts.md b/apps/ios/product/concepts.md new file mode 100644 index 0000000000..a60fe98cbb --- /dev/null +++ b/apps/ios/product/concepts.md @@ -0,0 +1,83 @@ +# SimpleX Chat iOS -- Concept Index + +> SimpleX Chat iOS concept index. Maps every product concept to its documentation and source code with bidirectional links. +> +> **Related spec:** [spec/api.md](../spec/api.md) | [spec/state.md](../spec/state.md) | [spec/architecture.md](../spec/architecture.md) + +## Table of Contents + +1. [Feature Concepts](#section-1-feature-concepts) +2. [Entity Index](#section-2-entity-index) + +## Executive Summary + +This document provides a structured mapping between product-level concepts, their documentation, and their implementation in both the Swift iOS layer and the Haskell core library. All source paths are relative to `apps/ios/` for Swift and use `../../src/` prefix for Haskell files (relative to `apps/ios/`). + +--- + +## Section 1: Feature Concepts + +| # | Concept | Product Docs | Spec Docs | Source Files (Swift) | Source Files (Haskell) | +|---|---------|-------------|-----------|---------------------|----------------------| +| 1 | Chat List | [views/chat-list.md](views/chat-list.md), [views/onboarding.md](views/onboarding.md) | [spec/client/chat-list.md](../spec/client/chat-list.md) | `Shared/Views/ChatList/ChatListView.swift` | `Controller.hs` (`APIGetChats`) | +| 2 | Direct Chat | [views/chat.md](views/chat.md), [flows/messaging.md](flows/messaging.md) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `Shared/Views/Chat/ChatView.swift`, `ChatInfoView.swift` | `Types.hs` (`Contact`), `Messages.hs` | +| 3 | Group Chat | [views/chat.md](views/chat.md), [views/group-info.md](views/group-info.md) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `Shared/Views/Chat/ChatView.swift`, `Group/GroupChatInfoView.swift` | `Types.hs` (`GroupInfo`, `GroupMember`) | +| 4 | Message Composition | [views/chat.md](views/chat.md) | [spec/client/compose.md](../spec/client/compose.md) | `ComposeMessage/ComposeView.swift`, `SendMessageView.swift` | `Controller.hs` (`APISendMessages`) | +| 5 | Message Reactions | [views/chat.md](views/chat.md) | [spec/api.md](../spec/api.md) | `ChatItem/EmojiItemView.swift` | `Controller.hs` (`APIChatItemReaction`) | +| 6 | Message Editing | [views/chat.md](views/chat.md) | [spec/client/compose.md](../spec/client/compose.md) | `ComposeMessage/ComposeView.swift`, `ChatItemInfoView.swift` | `Controller.hs` (`APIUpdateChatItem`) | +| 7 | Message Deletion | [views/chat.md](views/chat.md) | [spec/api.md](../spec/api.md) | `ChatItem/MarkedDeletedItemView.swift`, `DeletedItemView.swift` | `Controller.hs` (`APIDeleteChatItem`) | +| 8 | Timed Messages | [views/chat.md](views/chat.md) | [spec/api.md](../spec/api.md) | `ChatItem/CIChatFeatureView.swift` | `Types/Preferences.hs` (`TimedMessagesPreference`) | +| 9 | Voice Messages | [views/chat.md](views/chat.md) | [spec/client/compose.md](../spec/client/compose.md) | `ChatItem/CIVoiceView.swift`, `ComposeVoiceView.swift` | `Protocol.hs` (`MCVoice`) | +| 10 | File Transfer | [flows/file-transfer.md](flows/file-transfer.md) | [spec/services/files.md](../spec/services/files.md) | `ChatItem/CIFileView.swift`, `SimpleXChat/FileUtils.swift` | `Files.hs`, `Store/Files.hs` | +| 11 | Link Previews | [views/chat.md](views/chat.md) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `ChatItem/CILinkView.swift`, `ComposeLinkView.swift` | `Protocol.hs` (`MCLink`) | +| 12 | Contact Connection | [flows/connection.md](flows/connection.md), [views/new-chat.md](views/new-chat.md) | [spec/api.md](../spec/api.md) | `NewChat/NewChatView.swift`, `QRCode.swift` | `Controller.hs` (`APIConnect`, `APIAddContact`) | +| 13 | Contact Verification | [views/contact-info.md](views/contact-info.md) | [spec/api.md](../spec/api.md) | `Shared/Views/Chat/VerifyCodeView.swift` | `Controller.hs` (`APIVerifyContact`) | +| 14 | Group Management | [flows/group-lifecycle.md](flows/group-lifecycle.md) | [spec/api.md](../spec/api.md), [spec/database.md](../spec/database.md) | `NewChat/AddGroupView.swift`, `Group/GroupChatInfoView.swift` | `Controller.hs` (`APINewGroup`), `Store/Groups.hs` | +| 15 | Group Links | [views/group-info.md](views/group-info.md) | [spec/api.md](../spec/api.md) | `Group/GroupLinkView.swift` | `Controller.hs` (`APICreateGroupLink`) | +| 16 | Member Roles | [views/group-info.md](views/group-info.md) | [spec/api.md](../spec/api.md) | `SimpleXChat/ChatTypes.swift`, `Group/GroupMemberInfoView.swift` | `Types/Shared.hs` (`GroupMemberRole`) | +| 17 | Audio/Video Calls | [views/call.md](views/call.md), [flows/calling.md](flows/calling.md) | [spec/services/calls.md](../spec/services/calls.md) | `Call/ActiveCallView.swift`, `CallController.swift`, `WebRTCClient.swift` | `Call.hs` (`RcvCallInvitation`, `CallType`) | +| 18 | Push Notifications | [views/settings.md](views/settings.md) | [spec/services/notifications.md](../spec/services/notifications.md) | `Model/NtfManager.swift`, `SimpleX NSE/NotificationService.swift` | `Controller.hs` | +| 19 | User Profiles | [views/user-profiles.md](views/user-profiles.md) | [spec/state.md](../spec/state.md), [spec/client/navigation.md](../spec/client/navigation.md) | `UserSettings/UserProfilesView.swift`, `ChatList/UserPicker.swift` | `Types.hs` (`User`), `Store/Profiles.hs` | +| 20 | Incognito Mode | [views/contact-info.md](views/contact-info.md) | [spec/api.md](../spec/api.md) | `UserSettings/IncognitoHelp.swift` | `ProfileGenerator.hs`, `Types.hs` | +| 21 | Hidden Profiles | [views/user-profiles.md](views/user-profiles.md) | [spec/api.md](../spec/api.md) | `UserSettings/HiddenProfileView.swift` | `Controller.hs` (`APIHideUser`, `APIUnhideUser`) | +| 22 | Local Authentication | [views/settings.md](views/settings.md) | [spec/architecture.md](../spec/architecture.md) | `LocalAuth/LocalAuthView.swift`, `PasscodeView.swift` | N/A (iOS-only) | +| 23 | Database Encryption | [views/settings.md](views/settings.md) | [spec/database.md](../spec/database.md) | `Database/DatabaseEncryptionView.swift`, `DatabaseView.swift` | `Controller.hs` (`APIExportArchive`) | +| 24 | Theme System | [views/settings.md](views/settings.md) | [spec/services/theme.md](../spec/services/theme.md) | `Theme/ThemeManager.swift`, `SimpleXChat/Theme/ThemeTypes.swift` | `Types/UITheme.hs` | +| 25 | Network Configuration | [views/settings.md](views/settings.md) | [spec/architecture.md](../spec/architecture.md) | `NetworkAndServers/NetworkAndServers.swift`, `ProtocolServersView.swift` | `Controller.hs` (`APISetNetworkConfig`) | +| 26 | Device Migration | [flows/onboarding.md](flows/onboarding.md) | [spec/database.md](../spec/database.md) | `Migration/MigrateFromDevice.swift`, `MigrateToDevice.swift` | `Archive.hs` | +| 27 | Remote Desktop | [views/settings.md](views/settings.md) | [spec/architecture.md](../spec/architecture.md) | `RemoteAccess/ConnectDesktopView.swift` | `Remote.hs`, `Remote/Types.hs` | +| 28 | Chat Tags | [views/chat-list.md](views/chat-list.md) | [spec/state.md](../spec/state.md) | `ChatList/TagListView.swift`, `ChatListView.swift` | `Types.hs` (`ChatTag`), `Controller.hs` | +| 29 | User Address | [views/settings.md](views/settings.md) | [spec/api.md](../spec/api.md) | `UserSettings/UserAddressView.swift`, `Onboarding/AddressCreationCard.swift` | `Controller.hs` (`APICreateMyAddress`) | +| 30 | Member Support Chat | [views/group-info.md](views/group-info.md) | [spec/api.md](../spec/api.md) | `Group/MemberSupportView.swift`, `MemberAdmissionView.swift` | `Messages.hs` (`GroupChatScope`), `Controller.hs` | + +--- + +## Section 2: Entity Index + +Core data entities, their storage, and the operations that manage their lifecycle. + +| Entity | DB Table (Haskell) | Created By | Read By | Mutated By | Deleted By | +|--------|-------------------|------------|---------|------------|------------| +| **User** | `users` | `CreateActiveUser` in `Controller.hs` | `ListUsers`, `APISetActiveUser` in `Controller.hs` | `APISetActiveUser`, `APIHideUser`, `APIUnhideUser`, `APIMuteUser`, `APIUpdateProfile` in `Controller.hs` | `APIDeleteUser` in `Controller.hs`; `Store/Profiles.hs` | +| **Contact** | `contacts`, `contact_profiles` | `APIAddContact`, `APIConnect` in `Controller.hs` | `APIGetChat` in `Controller.hs`; `Store/Direct.hs` (getContact) | `APISetContactAlias`, `APISetConnectionAlias` in `Controller.hs`; `Store/Direct.hs` | `APIDeleteChat` in `Controller.hs`; `Store/Direct.hs` (deleteContact) | +| **GroupInfo** | `groups`, `group_profiles` | `APINewGroup` in `Controller.hs`; `Store/Groups.hs` (createNewGroup) | `APIGetChat`, `APIGroupInfo` in `Controller.hs`; `Store/Groups.hs` | `APIUpdateGroupProfile` in `Controller.hs`; `Store/Groups.hs` (updateGroupProfile) | `APIDeleteChat` in `Controller.hs`; `Store/Groups.hs` (deleteGroup) | +| **GroupMember** | `group_members`, `contact_profiles` | `APIAddMember`, `APIJoinGroup` in `Controller.hs`; `Store/Groups.hs` (createNewGroupMember) | `APIListMembers` in `Controller.hs`; `Store/Groups.hs` (getGroupMembers) | `APIMembersRole` in `Controller.hs`; `Store/Groups.hs` (updateGroupMemberRole) | `APIRemoveMembers` in `Controller.hs`; `Store/Groups.hs` (deleteGroupMember) | +| **ChatItem** | `chat_items`, `chat_item_versions` | `APISendMessages` in `Controller.hs`; `Store/Messages.hs` (createNewChatItem) | `APIGetChat`, `APIGetChatItems` in `Controller.hs`; `Store/Messages.hs` (getChatItems) | `APIUpdateChatItem`, `APIChatItemReaction` in `Controller.hs`; `Store/Messages.hs` (updateChatItem) | `APIDeleteChatItem` in `Controller.hs`; `Store/Messages.hs` (deleteChatItem) | +| **Connection** | `connections` | `createConnection` via SMP agent; `Store/Connections.hs` | `Store/Connections.hs` (getConnectionEntity) | `Store/Connections.hs` (updateConnectionStatus) | `Store/Connections.hs` (deleteConnection) | +| **FileTransfer** | `files`, `snd_files`, `rcv_files`, `xftp_file_descriptions` | `APISendMessages` (with file), `ReceiveFile` in `Controller.hs`; `Store/Files.hs` | `Store/Files.hs` (getFileTransfer) | `Store/Files.hs` (updateFileStatus, updateFileProgress) | `Store/Files.hs` (deleteFileTransfer) | +| **GroupLink** | `user_contact_links` | `APICreateGroupLink` in `Controller.hs`; `Store/Groups.hs` | `APIGetGroupLink` in `Controller.hs`; `Store/Groups.hs` | N/A (recreated on change) | `APIDeleteGroupLink` in `Controller.hs`; `Store/Groups.hs` | +| **ChatTag** | `chat_tags`, `chat_tags_chats` | `APICreateChatTag` in `Controller.hs` | `APIGetChats` in `Controller.hs` | `APIUpdateChatTag`, `APISetChatTags` in `Controller.hs` | `APIDeleteChatTag` in `Controller.hs` | +| **RcvCallInvitation** | In-memory (not persisted) | Received via `XCallInv` message in `Library/Subscriber.hs`; stored in `ChatModel.callInvitations` | `CallController.swift`, `IncomingCallView.swift` | Updated on call accept/reject in `CallManager.swift` | Removed on call end/reject; `Controller.hs` | + +--- + +## Cross-References + +- Product overview: [README.md](README.md) +- Glossary: [glossary.md](glossary.md) +- Haskell core controller: `../../src/Simplex/Chat/Controller.hs` +- Haskell core types: `../../src/Simplex/Chat/Types.hs` +- Haskell store layer: `../../src/Simplex/Chat/Store/` (Direct.hs, Groups.hs, Messages.hs, Files.hs, Profiles.hs, Connections.hs) +- Swift model: `Shared/Model/ChatModel.swift` +- Swift API types: `SimpleXChat/APITypes.swift`, `SimpleXChat/ChatTypes.swift` +- Swift API bridge: `SimpleXChat/API.swift`, `Shared/Model/SimpleXAPI.swift` diff --git a/apps/ios/product/flows/calling.md b/apps/ios/product/flows/calling.md new file mode 100644 index 0000000000..86cb026625 --- /dev/null +++ b/apps/ios/product/flows/calling.md @@ -0,0 +1,179 @@ +# Audio/Video Call Flow + +> **Related spec:** [spec/services/calls.md](../../spec/services/calls.md) + +## Overview + +WebRTC-based audio and video calling in SimpleX Chat iOS. Calls are end-to-end encrypted with an additional shared key negotiated over the E2E encrypted SMP channel. The iOS app integrates with CallKit for native call UI (incoming call screen, lock screen integration) with a fallback mode for regions where CallKit is restricted (China). Call signaling (offer/answer/ICE candidates) is exchanged via SMP messages, not through a central signaling server. + +## Prerequisites + +- Established direct contact connection (calls are 1:1 only, not available in groups) +- Microphone permission granted (audio calls) +- Camera permission granted (video calls) +- Network connectivity for WebRTC peer-to-peer or relay + +## Step-by-Step Processes + +### 1. Initiate Call + +1. User opens a direct chat in `ChatView`. +2. Taps the audio or video call button in the navigation bar. +3. `CallController` determines call type: `CallType(media: .audio/.video, capabilities: CallCapabilities(encryption: true))`. +4. If CallKit is enabled (`CallController.useCallKit()`): + - `CXStartCallAction` is requested via `CXCallController`. + - CallKit reports the outgoing call. + - `provider(perform: CXStartCallAction)` fulfills and reports `reportOutgoingCall(startedConnectingAt:)`. +5. Calls `apiSendCallInvitation(contact:callType:)`: + ```swift + func apiSendCallInvitation(_ contact: Contact, _ callType: CallType) async throws + ``` +6. Sends `ChatCommand.apiSendCallInvitation(contact:callType:)`. +7. Core sends the call invitation to the contact via SMP. +8. `ChatModel.shared.activeCall` is set with the call state. + +### 2. Receive Call + +1. `ChatReceiver` receives `ChatEvent.callInvitation(callInvitation: RcvCallInvitation)`. +2. `RcvCallInvitation` contains: `user`, `contact`, `callType`, `sharedKey`, `callUUID`, `callTs`. +3. Processing in `processReceivedMsg`: + - Call invitation is stored in `chatModel.callInvitations`. +4. If CallKit is enabled: + - `CXProvider.reportNewIncomingCall` presents the native iOS incoming call UI. + - Works even on lock screen and in background. +5. If CallKit is disabled (China / user preference): + - `IncomingCallView` is shown as an in-app overlay. + - `SoundPlayer` plays the ringtone. +6. User chooses to accept or reject. + +### 3. Accept Call + +1. **Via CallKit**: User swipes to accept on the native incoming call screen. + - `provider(perform: CXAnswerCallAction)` is triggered. + - Waits for chat to be started if needed (`waitUntilChatStarted(timeoutMs: 30_000)`). + - `callManager.answerIncomingCall(callUUID:)` begins WebRTC setup. + - `fulfillOnConnect` is set -- the action is fulfilled only when WebRTC reaches connected state (required for audio/mic to work on lock screen). +2. **Via in-app UI**: User taps "Accept" in `IncomingCallView`. + - Directly starts WebRTC setup. + +### 4. Reject Call + +1. **Via CallKit**: User taps "Decline" on native UI. + - `provider(perform: CXEndCallAction)` is triggered. + - `callManager.endCall(callUUID:)` cleans up. +2. **Via API**: `apiRejectCall(contact:)` sends rejection to peer. +3. Call invitation is removed from `chatModel.callInvitations`. + +### 5. WebRTC Setup (Signaling) + +All signaling messages are exchanged via E2E encrypted SMP messages (no central signaling server). + +**Caller side:** +1. `WebRTCClient` creates a `RTCPeerConnection`. +2. Creates SDP offer. +3. Calls `apiSendCallOffer(contact:rtcSession:rtcIceCandidates:media:capabilities:)`: + ```swift + func apiSendCallOffer(_ contact: Contact, _ rtcSession: String, _ rtcIceCandidates: String, + media: CallMediaType, capabilities: CallCapabilities) async throws + ``` +4. Constructs `WebRTCCallOffer(callType:rtcSession:)` and sends via `ChatCommand.apiSendCallOffer`. +5. Gathers ICE candidates and sends via `apiSendCallExtraInfo(contact:rtcIceCandidates:)`. + +**Callee side:** +1. Receives the offer via SMP. +2. `WebRTCClient` sets remote description from the offer. +3. Creates SDP answer. +4. Calls `apiSendCallAnswer(contact:rtcSession:rtcIceCandidates:)`: + ```swift + func apiSendCallAnswer(_ contact: Contact, _ rtcSession: String, _ rtcIceCandidates: String) async throws + ``` +5. Constructs `WebRTCSession(rtcSession:rtcIceCandidates:)` and sends. +6. Gathers and sends additional ICE candidates via `apiSendCallExtraInfo`. + +### 6. Media Streaming + +1. WebRTC peer connection transitions to connected state. +2. If CallKit is used, `fulfillOnConnect` action is fulfilled (enables audio hardware). +3. Audio/video streams are active. +4. `ActiveCallView` displays: + - Remote video (full screen) + - Local video preview (picture-in-picture corner) + - Call controls: mute, speaker, camera toggle, end call + - Call duration timer +5. `CallViewRenderers` manages WebRTC video rendering surfaces. +6. Call status updates are sent via `apiCallStatus(contact:status:)`. + +### 7. Audio Routing + +1. `CallAudioDeviceManager` handles audio device selection. +2. Options: earpiece (receiver), speaker, Bluetooth devices. +3. `AudioDevicePicker` provides UI for device selection during call. +4. Uses `AVAudioSession` for routing configuration. + +### 8. End Call + +1. Either party taps "End" button. +2. Calls `apiEndCall(contact:)`: + ```swift + func apiEndCall(_ contact: Contact) async throws + ``` +3. Sends `ChatCommand.apiEndCall(contact:)` via SMP to notify peer. +4. `WebRTCClient` closes peer connection, releases media resources. +5. If CallKit: `CXEndCallAction` is requested, `provider(perform: CXEndCallAction)` fulfills. +6. `ChatModel.shared.activeCall` is cleared. +7. A `CICallItemView` event item is added to the chat history (call duration, type). + +### 9. CallKit-Free Mode + +1. `CallController.isInChina` checks `SKStorefront().countryCode == "CHN"`. +2. If in China or user disabled CallKit (`callKitEnabledGroupDefault`): `useCallKit()` returns `false`. +3. Incoming calls use `IncomingCallView` overlay instead of native CallKit UI. +4. `SoundPlayer` handles ringtone playback. +5. No lock-screen call answering; app must be in foreground or notified via push. + +## Data Structures + +| Type | Location | Description | +|------|----------|-------------| +| `CallType` | `SimpleXChat/CallTypes.swift` | `media: CallMediaType` (.audio/.video), `capabilities: CallCapabilities` | +| `CallMediaType` | `SimpleXChat/CallTypes.swift` | `.audio` or `.video` | +| `CallCapabilities` | `SimpleXChat/CallTypes.swift` | `encryption: Bool` for E2E call encryption support | +| `RcvCallInvitation` | `SimpleXChat/CallTypes.swift` | Incoming call: user, contact, callType, sharedKey, callUUID, callTs | +| `WebRTCCallOffer` | `SimpleXChat/CallTypes.swift` | SDP offer with call type and WebRTC session data | +| `WebRTCSession` | `SimpleXChat/CallTypes.swift` | `rtcSession` (SDP) and `rtcIceCandidates` (serialized) | +| `WebRTCExtraInfo` | `SimpleXChat/CallTypes.swift` | Additional ICE candidates sent after initial offer/answer | +| `WebRTCCallStatus` | `SimpleXChat/CallTypes.swift` | Call lifecycle states for status reporting | +| `CallMediaSource` | `SimpleXChat/CallTypes.swift` | `.mic`, `.camera`, `.screenAudio`, `.screenVideo`, `.unknown` | +| `VideoCamera` | `SimpleXChat/CallTypes.swift` | `.user` (front) or `.environment` (rear) camera | + +## Error Cases + +| Error | Cause | Handling | +|-------|-------|----------| +| Chat not ready on CallKit answer | App suspended, slow startup | `waitUntilChatStarted` with 30s timeout; `action.fail()` on timeout | +| Call invitation not found | Race condition between notification and event processing | `justRefreshCallInvitations()` retry | +| WebRTC peer connection failure | NAT traversal, network issues | Call ends with error status | +| CallKit action fail | Internal state mismatch | `action.fail()` called, call cleaned up | +| No camera/mic permission | User denied permissions | Permission request dialog shown | + +## Key Files + +| File | Purpose | +|------|---------| +| `Shared/Views/Call/CallController.swift` | CallKit integration, CXProvider delegate, PKPushRegistry, call lifecycle management | +| `Shared/Views/Call/CallManager.swift` | Call state management, starting/answering/ending calls | +| `Shared/Views/Call/WebRTCClient.swift` | WebRTC peer connection, SDP offer/answer, ICE candidate handling | +| `Shared/Views/Call/ActiveCallView.swift` | Active call UI: video renderers, controls, duration | +| `Shared/Views/Call/CallViewRenderers.swift` | WebRTC video rendering surfaces | +| `Shared/Views/Call/IncomingCallView.swift` | Non-CallKit incoming call overlay | +| `Shared/Views/Call/CallAudioDeviceManager.swift` | Audio routing: speaker, earpiece, Bluetooth | +| `Shared/Views/Call/AudioDevicePicker.swift` | Audio device selection UI | +| `Shared/Views/Call/SoundPlayer.swift` | Ringtone and call sound playback | +| `Shared/Views/Call/WebRTC.swift` | WebRTC configuration and utilities | +| `SimpleXChat/CallTypes.swift` | All call-related type definitions | +| `Shared/Model/SimpleXAPI.swift` | Call API functions: `apiSendCallInvitation`, `apiSendCallOffer`, `apiSendCallAnswer`, `apiSendCallExtraInfo`, `apiEndCall`, `apiRejectCall`, `apiCallStatus` | + +## Related Specifications + +- `apps/ios/product/README.md` -- Product overview: Calls capability +- `apps/ios/product/flows/connection.md` -- Calls require an established direct connection diff --git a/apps/ios/product/flows/connection.md b/apps/ios/product/flows/connection.md new file mode 100644 index 0000000000..7b9c8ee304 --- /dev/null +++ b/apps/ios/product/flows/connection.md @@ -0,0 +1,159 @@ +# Connection Flow + +> **Related spec:** [spec/api.md](../../spec/api.md) | [spec/architecture.md](../../spec/architecture.md) + +## Overview + +Establishing contact between two SimpleX Chat users. SimpleX uses no user identifiers; connections are formed through one-time invitation links or permanent SimpleX addresses. Each connection creates unique unidirectional SMP queues, ensuring no server can correlate sender and receiver. Supports incognito mode for per-contact random profile generation. + +## Prerequisites + +- User profile created and chat engine running +- Network connectivity to SMP relay servers +- For QR code scanning: camera permission granted + +## Step-by-Step Processes + +### 1. Create Invitation Link + +1. User taps "+" button in `ChatListView` -> `NewChatMenuButton` -> "Add contact". +2. `NewChatView` is presented. +3. Calls `apiAddContact(incognito:)`: + ```swift + func apiAddContact(incognito: Bool) async + -> ((CreatedConnLink, PendingContactConnection)?, Alert?) + ``` +4. Internally sends `ChatCommand.apiAddContact(userId:incognito:)` to core. +5. Core creates SMP queues and returns `ChatResponse1.invitation(user, connLinkInv, connection)`. +6. Returns `(CreatedConnLink, PendingContactConnection)`. +7. `CreatedConnLink` contains the invitation URI (both full and short link forms). +8. UI displays: + - QR code rendered by `QRCode` view (scannable by peer) + - Share button to send link via system share sheet + - Copy button for clipboard +9. A `PendingContactConnection` appears in the chat list while awaiting peer. + +### 2. Connect via Link + +1. User receives a SimpleX link (pasted, scanned, or opened via URL scheme). +2. If opened via deep link: `SimpleXApp.onOpenURL` sets `chatModel.appOpenUrl`. +3. For manual entry: User pastes link in `NewChatView`. +4. First, `apiConnectPlan(connLink:inProgress:)` is called to validate: + ```swift + func apiConnectPlan(connLink: String, inProgress: BoxedValue) async + -> ((CreatedConnLink, ConnectionPlan)?, Alert?) + ``` +5. Returns `ConnectionPlan` indicating whether it is an invitation, contact address, or group link, and whether connection is already established. +6. If valid, calls `apiConnect(incognito:connLink:)`: + ```swift + func apiConnect(incognito: Bool, connLink: CreatedConnLink) async + -> (ConnReqType, PendingContactConnection)? + ``` +7. Core creates the connection and returns one of: + - `ChatResponse1.sentConfirmation(user, connection)` -- for invitation links (type: `.invitation`) + - `ChatResponse1.sentInvitation(user, connection)` -- for contact address links (type: `.contact`) + - `ChatResponse1.contactAlreadyExists(user, contact)` -- duplicate +8. `PendingContactConnection` appears in chat list while awaiting peer confirmation. + +### 3. Prepared Contact/Group Flow (Short Links) + +1. For short links with embedded profile data, the app uses a two-phase flow. +2. `apiPrepareContact(connLink:contactShortLinkData:)` or `apiPrepareGroup(connLink:groupShortLinkData:)` creates a local prepared chat. +3. Returns `ChatData` with the prepared contact/group shown in UI before connecting. +4. User can switch profiles or set incognito before committing. +5. `apiConnectPreparedContact(contactId:incognito:msg:)` finalizes the connection. +6. Returns `ChatResponse1.startedConnectionToContact(user, contact)`. + +### 4. Accept Contact Request + +1. When a peer connects via the user's SimpleX address, core generates a `ChatEvent.receivedContactRequest`. +2. `processReceivedMsg` handles the event, adding a `UserContactRequest` to `ChatModel`. +3. Contact request appears in `ChatListView` as a special `ContactRequestView` row. +4. User taps "Accept": + ```swift + func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? + ``` +5. Sends `ChatCommand.apiAcceptContact(incognito:contactReqId:)`. +6. Core returns `ChatResponse1.acceptingContactRequest(user, contact)`. +7. Connection handshake proceeds asynchronously. +8. User can also reject: `apiRejectContactRequest(contactReqId:)` -> `ChatResponse1.contactRequestRejected`. + +### 5. Connection Established + +1. Both sides complete the SMP handshake asynchronously. +2. Core sends `ChatEvent.contactConnected(user, contact, userCustomProfile)`. +3. `processReceivedMsg` updates `ChatModel`: + - Contact status transitions from pending to active. + - Chat becomes available for messaging. +4. `NtfManager` may post a notification: "Contact connected". +5. The `PendingContactConnection` in the chat list is replaced by the full contact chat. + +### 6. Create SimpleX Address + +1. User navigates to Settings or taps "Create SimpleX address" during onboarding. +2. Calls `apiCreateUserAddress()`: + ```swift + func apiCreateUserAddress() async throws -> CreatedConnLink? + ``` +3. Core creates a permanent address (unlike one-time invitations). +4. Address is stored in `ChatModel.shared.userAddress`. +5. Can be shared publicly; multiple contacts can connect via the same address. +6. User must accept each incoming contact request individually. +7. To delete: `apiDeleteUserAddress()` removes the address and associated SMP queues. + +### 7. Incognito Connection + +1. Before connecting, user toggles "Incognito" in the connection UI. +2. `incognito: true` is passed to `apiAddContact`, `apiConnect`, or `apiAcceptContactRequest`. +3. Core generates a random display name for this connection only. +4. The random profile is stored per-connection; the user's real profile is never shared. +5. Incognito status is shown with a mask icon in the chat. +6. Can also be toggled for pending connections via `apiSetConnectionIncognito(connId:incognito:)`. + +## Data Structures + +| Type | Location | Description | +|------|----------|-------------| +| `CreatedConnLink` | `SimpleXChat/APITypes.swift` | Contains `connFullLink` (URI) and optional `connShortLink` | +| `PendingContactConnection` | `SimpleXChat/ChatTypes.swift` | Represents an in-progress connection before contact is established | +| `ConnectionPlan` | `Shared/Model/AppAPITypes.swift` | Enum describing what a link will do: connect contact, join group, already connected, etc. | +| `ConnReqType` | `Shared/Views/NewChat/NewChatView.swift` | `.invitation`, `.contact`, or `.groupLink` -- type of connection request | +| `Contact` | `SimpleXChat/ChatTypes.swift` | Full contact model with profile, connection status, preferences | +| `UserContactRequest` | `SimpleXChat/ChatTypes.swift` | Incoming contact request awaiting acceptance | +| `ChatType` | `SimpleXChat/ChatTypes.swift` | `.direct`, `.group`, `.local`, `.contactRequest`, `.contactConnection` | + +## Error Cases + +| Error | Cause | Handling | +|-------|-------|----------| +| `ChatError.invalidConnReq` | Malformed or expired link | Alert: "Invalid connection link" | +| `ChatError.unsupportedConnReq` | Link requires newer app version | Alert: "Unsupported connection link" | +| `ChatError.errorAgent(.SMP(_, .AUTH))` | Link already used or deleted | Alert: "Connection error (AUTH)" | +| `ChatError.errorAgent(.SMP(_, .BLOCKED(info)))` | Server operator blocked connection | Alert: "Connection blocked" with reason | +| `ChatError.errorAgent(.SMP(_, .QUOTA))` | Too many undelivered messages | Alert: "Undelivered messages" | +| `ChatError.errorAgent(.INTERNAL("SEUniqueID"))` | Duplicate connection attempt | Alert: "Already connected?" | +| `ChatError.errorAgent(.BROKER(_, .TIMEOUT))` | Server timeout | Retryable via `chatApiSendCmdWithRetry` | +| `ChatError.errorAgent(.BROKER(_, .NETWORK))` | Network failure | Retryable via `chatApiSendCmdWithRetry` | +| `contactAlreadyExists` | Connecting to existing contact | Alert: "Contact already exists" with contact name | +| `errorAgent(.SMP(_, .AUTH))` on accept | Sender deleted request | Alert: "Sender may have deleted the connection request" | + +## Key Files + +| File | Purpose | +|------|---------| +| `Shared/Views/NewChat/NewChatView.swift` | Main connection UI: create link, paste link, QR scan | +| `Shared/Views/NewChat/NewChatMenuButton.swift` | "+" button menu in chat list | +| `Shared/Views/NewChat/QRCode.swift` | QR code rendering for invitation links | +| `Shared/Views/NewChat/AddContactLearnMore.swift` | Help text explaining connection process | +| `Shared/Views/ChatList/ContactRequestView.swift` | Incoming contact request display | +| `Shared/Views/ChatList/ContactConnectionView.swift` | Pending connection display | +| `Shared/Views/ChatList/ContactConnectionInfo.swift` | Connection details sheet | +| `Shared/Model/SimpleXAPI.swift` | API functions: `apiAddContact`, `apiConnect`, `apiConnectPlan`, `apiAcceptContactRequest`, `apiCreateUserAddress` | +| `Shared/Model/AppAPITypes.swift` | `ConnectionPlan` enum, `GroupLink` struct | +| `SimpleXChat/APITypes.swift` | `CreatedConnLink`, `ComposedMessage`, command/response types | +| `SimpleXChat/ChatTypes.swift` | `Contact`, `PendingContactConnection`, `UserContactRequest` | + +## Related Specifications + +- `apps/ios/product/README.md` -- Product overview: Contacts capability map +- `apps/ios/product/flows/messaging.md` -- Messaging after connection is established diff --git a/apps/ios/product/flows/file-transfer.md b/apps/ios/product/flows/file-transfer.md new file mode 100644 index 0000000000..0b4b0538cc --- /dev/null +++ b/apps/ios/product/flows/file-transfer.md @@ -0,0 +1,209 @@ +# File Transfer Flow + +> **Related spec:** [spec/services/files.md](../../spec/services/files.md) + +## Overview + +File and media sharing in SimpleX Chat iOS. Small files are sent inline within SMP messages; large files use the XFTP (eXtended File Transfer Protocol) for chunked, encrypted uploads up to 1GB. All files are encrypted end-to-end. Optional local encryption protects downloaded files at rest using AES via `CryptoFile`. + +## Prerequisites + +- Established contact or group conversation +- For sending: photo library or file picker access permission +- For receiving: sufficient device storage +- XFTP relay servers configured (default servers or custom) + +## Size Limits + +| Category | Limit | Constant | +|----------|-------|----------| +| Inline image (compressed) | 255 KB | `MAX_IMAGE_SIZE` = 261,120 bytes | +| Auto-receive image | 510 KB | `MAX_IMAGE_SIZE_AUTO_RCV` = MAX_IMAGE_SIZE * 2 | +| Auto-receive voice | 510 KB | `MAX_VOICE_SIZE_AUTO_RCV` = MAX_IMAGE_SIZE * 2 | +| Auto-receive video | 1,023 KB | `MAX_VIDEO_SIZE_AUTO_RCV` = 1,047,552 bytes | +| Max file via XFTP | 1 GB | `MAX_FILE_SIZE_XFTP` = 1,073,741,824 bytes | +| Max file via SMP | ~8 MB | `MAX_FILE_SIZE_SMP` = 8,000,000 bytes | +| Max voice message length | 5 min | `MAX_VOICE_MESSAGE_LENGTH` = 300s | + +## Step-by-Step Processes + +### 1. Send Image + +1. User taps the attachment button in `ComposeView` and selects an image. +2. `ComposeImageView` displays the selected image preview. +3. Image is compressed to fit within `MAX_IMAGE_SIZE` (255KB). +4. `ComposedMessage` is built: + ```swift + ComposedMessage( + fileSource: CryptoFile(filePath: compressedImagePath), + msgContent: .image(text: captionText, image: base64Thumbnail) + ) + ``` +5. `apiSendMessages(type:id:scope:composedMessages:)` is called. +6. For images <=255KB: sent inline within the SMP message. +7. For larger images: XFTP upload is used (see XFTP transfer below). +8. Recipient auto-receives images up to 510KB (`MAX_IMAGE_SIZE_AUTO_RCV`). + +### 2. Send Video + +1. User picks a video from the library. +2. Thumbnail is generated from the first frame. +3. Video duration is calculated. +4. `ComposedMessage` is built: + ```swift + ComposedMessage( + fileSource: CryptoFile(filePath: videoFilePath), + msgContent: .video(text: captionText, image: base64Thumbnail, duration: durationSeconds) + ) + ``` +5. `apiSendMessages(...)` is called. +6. Video files are typically >255KB, so XFTP upload is used. +7. Recipient auto-receives videos up to 1,023KB (`MAX_VIDEO_SIZE_AUTO_RCV`). +8. `CIVideoView` displays thumbnail with play button; video downloads on tap if not auto-received. + +### 3. Send File + +1. User taps the attachment button and selects a document via the system file picker. +2. `ComposeFileView` shows the file name and size. +3. `ComposedMessage` is built: + ```swift + ComposedMessage( + fileSource: CryptoFile(filePath: filePath), + msgContent: .file(fileName) + ) + ``` +4. `apiSendMessages(...)` is called. +5. If file <=255KB: sent inline via SMP. +6. If file >255KB and <=1GB: uploaded via XFTP. +7. Files >1GB: rejected (prevented in UI). +8. `CIFileView` displays file icon, name, and size for the recipient. + +### 4. Send Voice Message + +1. User taps and holds the microphone button in `ComposeView`. +2. `AudioRecPlay` records audio to a temporary file. +3. `ComposeVoiceView` shows recording waveform and duration. +4. On release (or tapping stop), recording ends. +5. Duration is checked against `MAX_VOICE_MESSAGE_LENGTH` (5 minutes / 300 seconds). +6. `ComposedMessage` is built: + ```swift + ComposedMessage( + fileSource: CryptoFile(filePath: voiceFilePath), + msgContent: .voice(text: "", duration: durationSeconds) + ) + ``` +7. `apiSendMessages(...)` is called. +8. Voice messages <=510KB are sent inline. +9. Recipient auto-receives voice up to 510KB (`MAX_VOICE_SIZE_AUTO_RCV`). +10. `CIVoiceView` renders waveform with playback controls. + +### 5. Receive File + +1. Core receives a message with a file reference via SMP. +2. `ChatEvent.newChatItems` delivers the chat item with file metadata. +3. Auto-receive logic checks: + - File type and size against auto-receive thresholds. + - User's auto-receive preferences. +4. If auto-received or user taps "Download": + ```swift + func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async + ``` +5. Internally calls `receiveFiles(user:fileIds:userApprovedRelays:auto:)`. +6. Sends `ChatCommand.receiveFile(fileId:userApprovedRelays:encrypted:inline:)`. +7. `encrypted` is determined by `privacyEncryptLocalFilesGroupDefault`. +8. `userApprovedRelays` controls whether unknown XFTP relay servers are trusted. +9. On success: `ChatResponse2.rcvFileAccepted(user, chatItem)` -- file download begins. +10. On sender cancelled: `ChatResponse2.rcvFileAcceptedSndCancelled(user, rcvFileTransfer)`. +11. Download progress is tracked and shown in the UI. +12. Completed files are stored in the app's `Documents/files/` directory. + +### 6. XFTP Transfer (Large Files) + +**Upload (sender side):** +1. File is encrypted locally with a random symmetric key. +2. Encrypted file is split into chunks. +3. Chunks are uploaded to one or more XFTP relay servers. +4. A file description (URI with encryption key and chunk locations) is created. +5. The file description is sent to the recipient via the SMP message. + +**Download (recipient side):** +1. Recipient receives the file description via SMP. +2. Chunks are downloaded from XFTP relay servers. +3. Chunks are reassembled and decrypted locally. +4. File is available at the local path. + +**Standalone file operations** (used for database migration): +- `uploadStandaloneFile(user:file:ctrl:)` -- upload without a chat message +- `downloadStandaloneFile(user:url:file:ctrl:)` -- download from a standalone URL +- `standaloneFileInfo(url:ctrl:)` -- get metadata for a standalone file URL + +### 7. Local File Encryption + +1. If `privacyEncryptLocalFilesGroupDefault` is enabled in privacy settings: + - Downloaded files are encrypted at rest using AES via `CryptoFile`. + - `CryptoFile` wraps a file path with encryption metadata. +2. Encryption key is derived and stored securely. +3. Files are decrypted on-the-fly when accessed for viewing/playback. +4. This protects files even if the device storage is accessed externally. + +### 8. Unknown Relay Server Approval + +1. When receiving a file, XFTP relay servers are checked against known/approved servers. +2. If unknown servers are detected: `ChatError.error(.fileNotApproved(fileId, unknownServers))`. +3. If not auto-receiving, user is shown an alert: + - "Unknown servers! Without Tor or VPN, your IP address will be visible to these XFTP relays: [server list]." + - Option to "Download" (approve) or cancel. +4. On approval: `receiveFiles(user:fileIds:userApprovedRelays: true)` retries with approval. +5. If `privacyAskToApproveRelaysGroupDefault` is disabled, relays are auto-approved. + +## Data Structures + +| Type | Location | Description | +|------|----------|-------------| +| `CryptoFile` | `SimpleXChat/CryptoFile.swift` | File path with optional encryption key and nonce for local AES encryption | +| `MsgContent.image` | `SimpleXChat/ChatTypes.swift` | `.image(text: String, image: String)` -- text caption + base64 thumbnail | +| `MsgContent.video` | `SimpleXChat/ChatTypes.swift` | `.video(text: String, image: String, duration: Int)` -- caption + thumbnail + duration | +| `MsgContent.voice` | `SimpleXChat/ChatTypes.swift` | `.voice(text: String, duration: Int)` -- empty text + duration in seconds | +| `MsgContent.file` | `SimpleXChat/ChatTypes.swift` | `.file(String)` -- file name | +| `ComposedMessage` | `SimpleXChat/APITypes.swift` | Outgoing message with fileSource, quotedItemId, msgContent, mentions | +| `FileTransferMeta` | `SimpleXChat/ChatTypes.swift` | Metadata for an ongoing file transfer | +| `RcvFileTransfer` | `SimpleXChat/ChatTypes.swift` | State of a file being received | +| `MigrationFileLinkData` | Used for standalone file transfers during database migration | + +## Error Cases + +| Error | Cause | Handling | +|-------|-------|----------| +| `fileNotApproved(fileId, unknownServers)` | Unknown XFTP relay servers | Alert with option to approve and retry | +| `fileCancelled` | File transfer was cancelled | Silently ignored in `receiveFiles` | +| `fileAlreadyReceiving` | Duplicate receive request | Silently ignored | +| `rcvFileAcceptedSndCancelled` | Sender cancelled after acceptance | Alert: "Sender cancelled file transfer" | +| File too large | Exceeds 1GB XFTP limit | Prevented in UI picker | +| Network errors | XFTP server unreachable | Standard retry mechanism | +| Storage full | Insufficient device storage | System-level error | + +## Key Files + +| File | Purpose | +|------|---------| +| `SimpleXChat/FileUtils.swift` | File size constants, path utilities, database file management | +| `SimpleXChat/CryptoFile.swift` | Local file encryption/decryption with AES | +| `SimpleXChat/ImageUtils.swift` | Image compression and thumbnail generation | +| `Shared/Views/Chat/ComposeMessage/ComposeView.swift` | File/media attachment selection and composition | +| `Shared/Views/Chat/ComposeMessage/ComposeImageView.swift` | Image preview in compose area | +| `Shared/Views/Chat/ComposeMessage/ComposeFileView.swift` | File preview in compose area | +| `Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift` | Voice recording UI with waveform | +| `Shared/Views/Chat/ChatItem/CIFileView.swift` | File message display: icon, name, size, download action | +| `Shared/Views/Chat/ChatItem/CIImageView.swift` | Image message display: thumbnail, full-screen tap | +| `Shared/Views/Chat/ChatItem/CIVideoView.swift` | Video message display: thumbnail, play button, inline playback | +| `Shared/Views/Chat/ChatItem/CIVoiceView.swift` | Voice message display: waveform, playback controls | +| `Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift` | Voice message inside a framed (quoted/forwarded) context | +| `Shared/Views/Chat/ChatItem/FullScreenMediaView.swift` | Full-screen image/video viewer | +| `Shared/Model/SimpleXAPI.swift` | `apiSendMessages`, `receiveFile`, `receiveFiles`, `uploadStandaloneFile`, `downloadStandaloneFile` | +| `Shared/Model/AudioRecPlay.swift` | Audio recording and playback engine for voice messages | + +## Related Specifications + +- `apps/ios/product/README.md` -- Product overview: Messaging capability (file sharing) +- `apps/ios/product/flows/messaging.md` -- File transfer is part of the message send flow +- `apps/ios/product/views/chat.md` -- Chat view file/media display diff --git a/apps/ios/product/flows/group-lifecycle.md b/apps/ios/product/flows/group-lifecycle.md new file mode 100644 index 0000000000..78d4f28738 --- /dev/null +++ b/apps/ios/product/flows/group-lifecycle.md @@ -0,0 +1,216 @@ +# Group Lifecycle Flow + +> **Related spec:** [spec/api.md](../../spec/api.md) | [spec/database.md](../../spec/database.md) + +## Overview + +Complete group management in SimpleX Chat iOS: creating groups, inviting members, joining via links, managing roles and admission, and group deletion. Groups use the same E2E encryption as direct messages -- each member pair has independent encrypted channels. Group metadata (name, image, preferences) is distributed via the group protocol. + +## Prerequisites + +- User profile created and chat engine running +- At least one established contact (to invite to a group) +- For joining via link: a valid group link or invitation + +## Step-by-Step Processes + +### 1. Create Group + +1. User taps "+" in `ChatListView` -> `NewChatMenuButton` -> "Create group". +2. `AddGroupView` is presented for entering group name, optional image, and description. +3. User fills in `GroupProfile(displayName:fullName:image:description:)` and taps "Create". +4. Calls `apiNewGroup(incognito:groupProfile:)`: + ```swift + func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInfo + ``` +5. Sends `ChatCommand.apiNewGroup(userId:incognito:groupProfile:)` to core (synchronous). +6. Core returns `ChatResponse2.groupCreated(user, groupInfo)`. +7. `GroupInfo` contains the new group's ID, profile, and the creator as owner. +8. User is navigated to `AddGroupMembersView` to optionally invite contacts. +9. User can also create a group link at this stage. + +### 2. Invite Members + +1. From `GroupChatInfoView`, user taps "Add members" -> `AddGroupMembersView`. +2. `filterMembersToAdd` filters contacts already in the group. +3. User selects contacts and assigns roles (default: `.member`). +4. For each selected contact, calls `apiAddMember(groupId:contactId:memberRole:)`: + ```swift + func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember + ``` +5. Core sends group invitation to the contact and returns `ChatResponse2.sentGroupInvitation(user, _, _, member)`. +6. The invited contact receives a `CIGroupInvitationView` in their chat. +7. Invited member's status is `.invited` until they accept. + +### 3. Join via Link + +1. User receives a group link (scanned or pasted). +2. `apiConnectPlan` validates the link and identifies it as a group link. +3. For prepared groups (short links): `apiPrepareGroup(connLink:groupShortLinkData:)` shows group info before joining. +4. `apiConnectPreparedGroup(groupId:incognito:msg:)` or `apiConnect(incognito:connLink:)` initiates joining. +5. Core processes the join request. Depending on group admission settings: + - **Auto-join**: Member is added immediately. + - **Approval required**: Member enters pending admission queue. +6. `apiJoinGroup(groupId:)` is called for invitation-based joins: + ```swift + func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult? + ``` +7. Returns one of: + - `.joined(groupInfo:)` -- successfully joined + - `.invitationRemoved` -- invitation was revoked (SMP AUTH error) + - `.groupNotFound` -- group no longer exists + +### 4. Member Admission + +1. Group has admission settings configured via `MemberAdmissionView`. +2. When a new member joins a group requiring approval, admins see pending members. +3. Admin reviews pending member in the member list. +4. To accept: `apiAcceptMember(groupId:groupMemberId:memberRole:)`: + ```swift + func apiAcceptMember(_ groupId: Int64, _ groupMemberId: Int64, _ memberRole: GroupMemberRole) async throws -> (GroupInfo, GroupMember) + ``` +5. Core returns `ChatResponse2.memberAccepted(user, groupInfo, member)`. +6. To reject: remove the pending member (same as member removal). +7. Member support chat (`MemberSupportView`, `MemberSupportChatToolbar`) allows admins to communicate with pending members. + +### 5. Change Member Roles + +1. Admin/owner navigates to member info in `GroupChatInfoView`. +2. Selects new role for the member. +3. Calls `apiMembersRole(groupId:memberIds:memberRole:)`: + ```swift + func apiMembersRole(_ groupId: Int64, _ memberIds: [Int64], _ memberRole: GroupMemberRole) async throws -> [GroupMember] + ``` +4. Core returns `ChatResponse2.membersRoleUser(user, _, members, _)`. +5. Available roles (in hierarchy order): + - `.owner` -- full control, can delete group + - `.admin` -- can manage members, change roles (below admin) + - `.moderator` -- can delete messages, moderate content + - `.member` -- standard participant, can send messages + - `.observer` -- read-only access +6. Role changes are broadcast to all group members as group events. + +### 6. Remove Member + +1. Admin/owner navigates to member info -> taps "Remove". +2. Calls `apiRemoveMembers(groupId:memberIds:withMessages:)`: + ```swift + func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool) async throws -> (GroupInfo, [GroupMember]) + ``` +3. `withMessages: true` also deletes all messages from that member. +4. Core returns `ChatResponse2.userDeletedMembers(user, updatedGroupInfo, members, withMessages)`. +5. Removed member receives notification and loses access. + +### 7. Block Member for All + +1. Admin can block a member's messages from being visible to all group members. +2. Calls `apiBlockMembersForAll(groupId:memberIds:blocked:)`: + ```swift + func apiBlockMembersForAll(_ groupId: Int64, _ memberIds: [Int64], _ blocked: Bool) async throws -> [GroupMember] + ``` +3. Core returns `ChatResponse2.membersBlockedForAllUser(user, _, members, _)`. + +### 8. Leave Group + +1. User navigates to `GroupChatInfoView` -> taps "Leave group". +2. Confirmation dialog is presented. +3. Calls `leaveGroup(groupId:)` which wraps `apiLeaveGroup(groupId:)`: + ```swift + func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo + ``` +4. Core returns `ChatResponse2.leftMemberUser(user, groupInfo)`. +5. `ChatModel.shared.updateGroup(groupInfo)` updates the UI. +6. User retains local chat history but can no longer send/receive. + +### 9. Delete Group + +1. Owner navigates to `GroupChatInfoView` -> taps "Delete group". +2. Calls `apiDeleteChat(type: .group, id: groupId)`: + ```swift + func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws + ``` +3. Core notifies all members and removes the group. +4. Chat is removed from `ChatModel.shared.chats`. + +### 10. Group Link Management + +**Create group link:** +1. From `GroupLinkView` (accessible via `GroupChatInfoView`). +2. Calls `apiCreateGroupLink(groupId:memberRole:)`: + ```swift + func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink? + ``` +3. Returns `GroupLink` containing the link URI and member role. +4. Optional: `apiAddGroupShortLink(groupId:)` generates an additional short link. + +**Update link role:** +- `apiGroupLinkMemberRole(groupId:memberRole:)` changes the default role for new joiners. + +**Delete group link:** +- `apiDeleteGroupLink(groupId:)` invalidates the link. + +**Get existing link:** +- `apiGetGroupLink(groupId:)` retrieves the current link (returns `nil` if none exists). + +### 11. Group Preferences + +1. `GroupPreferencesView` allows configuring per-feature preferences. +2. Features controlled include: + - Timed/disappearing messages + - Message reactions + - Voice messages + - File sharing + - Direct messages between members + - Full message deletion + - Message history visibility for new members +3. Changes are saved via `apiUpdateGroup(groupId:groupProfile:)` with updated preferences. +4. `GroupWelcomeView` manages the welcome message shown to new joiners. + +## Data Structures + +| Type | Location | Description | +|------|----------|-------------| +| `GroupInfo` | `SimpleXChat/ChatTypes.swift` | Full group model: ID, profile, membership, preferences, business chat info | +| `GroupProfile` | `SimpleXChat/ChatTypes.swift` | Name, full name, image, description, preferences | +| `GroupMember` | `SimpleXChat/ChatTypes.swift` | Member model: role, status, profile, connection info | +| `GroupMemberRole` | `SimpleXChat/ChatTypes.swift` | `.owner`, `.admin`, `.moderator`, `.member`, `.observer` | +| `GroupMemberStatus` | `SimpleXChat/ChatTypes.swift` | Member lifecycle: `.invited`, `.accepted`, `.connected`, `.complete`, etc. | +| `GroupLink` | `Shared/Model/AppAPITypes.swift` | Group link with URI, member role, and short link data | +| `BusinessChatInfo` | `SimpleXChat/ChatTypes.swift` | Business chat metadata for commercial group chats | +| `JoinGroupResult` | `Shared/Model/SimpleXAPI.swift` | `.joined(groupInfo)`, `.invitationRemoved`, `.groupNotFound` | +| `GMember` | `Shared/Views/Chat/Group/` | View-layer wrapper around `GroupMember` for list display | + +## Error Cases + +| Error | Cause | Handling | +|-------|-------|----------| +| `errorStore(.groupNotFound)` | Group deleted or not accessible | `JoinGroupResult.groupNotFound` | +| `errorAgent(.SMP(_, .AUTH))` | Invitation revoked | `JoinGroupResult.invitationRemoved` | +| `errorStore(.groupLinkNotFound)` | No group link exists | `apiGetGroupLink` returns `nil` | +| `duplicateGroupLink` | Link already exists for group | Show alert | +| `errorAgent(.NOTICE(server, preset, expires))` | Server notice during link creation | `showClientNotice` alert | +| Network errors | SMP/XFTP server unreachable | Retryable via `chatApiSendCmdWithRetry` | + +## Key Files + +| File | Purpose | +|------|---------| +| `Shared/Views/NewChat/AddGroupView.swift` | Group creation UI | +| `Shared/Views/Chat/Group/AddGroupMembersView.swift` | Member invitation UI | +| `Shared/Views/Chat/Group/GroupLinkView.swift` | Group link management UI | +| `Shared/Views/Chat/Group/GroupProfileView.swift` | Group profile editing | +| `Shared/Views/Chat/Group/GroupPreferencesView.swift` | Feature preferences UI | +| `Shared/Views/Chat/Group/GroupWelcomeView.swift` | Welcome message editing | +| `Shared/Views/Chat/Group/MemberAdmissionView.swift` | Admission settings UI | +| `Shared/Views/Chat/Group/MemberSupportView.swift` | Admin-to-pending-member chat | +| `Shared/Views/Chat/Group/MemberSupportChatToolbar.swift` | Support chat accept/reject toolbar | +| `Shared/Views/Chat/Group/SecondaryChatView.swift` | Secondary chat view for member support | +| `Shared/Model/SimpleXAPI.swift` | All group API functions | +| `Shared/Model/AppAPITypes.swift` | `GroupLink`, `ConnectionPlan` | +| `SimpleXChat/ChatTypes.swift` | `GroupInfo`, `GroupProfile`, `GroupMember`, `GroupMemberRole` | + +## Related Specifications + +- `apps/ios/product/README.md` -- Product overview: Groups capability map +- `apps/ios/product/flows/connection.md` -- Connection flow (group links use the same connect mechanism) +- `apps/ios/product/flows/messaging.md` -- Messaging within groups diff --git a/apps/ios/product/flows/messaging.md b/apps/ios/product/flows/messaging.md new file mode 100644 index 0000000000..527079995c --- /dev/null +++ b/apps/ios/product/flows/messaging.md @@ -0,0 +1,178 @@ +# Messaging Flow + +> **Related spec:** [spec/api.md](../../spec/api.md) | [spec/client/chat-view.md](../../spec/client/chat-view.md) | [spec/client/compose.md](../../spec/client/compose.md) + +## Overview + +Complete message lifecycle in SimpleX Chat iOS: composing, sending, receiving, editing, deleting, reacting to, replying to, and forwarding messages. All messages are end-to-end encrypted via the SMP protocol. The Haskell core handles encryption, routing, and persistence; the Swift UI layer drives composition and display. + +## Prerequisites + +- User profile created and chat engine running (`startChat()` completed) +- At least one established contact or group conversation +- `ChatModel.shared` populated with chat list data + +## Step-by-Step Processes + +### 1. Send Text Message + +1. User navigates to a conversation (direct or group) via `ChatListView` -> `ChatView`. +2. User types text into `ComposeView`'s `SendMessageView` text editor. +3. Link previews are detected and fetched asynchronously (`ComposeLinkView`). +4. User taps the send button. +5. `ComposeView` builds a `ComposedMessage`: + ```swift + ComposedMessage( + fileSource: nil, + quotedItemId: nil, + msgContent: .text("Hello"), + mentions: [:] + ) + ``` +6. Calls `apiSendMessages(type:id:scope:live:ttl:composedMessages:)`. +7. Internally dispatches `ChatCommand.apiSendMessages(...)` to the Haskell core. +8. Core encrypts, queues via SMP, and returns `ChatResponse1.newChatItems(user, aChatItems)`. +9. `processSendMessageCmd` extracts `[ChatItem]` from response. +10. For direct chats, a background task tracks delivery via `chatModel.messageDelivery`. +11. `ChatModel` updates, UI refreshes to show the new message. + +### 2. Send Media (Image/Video/File) + +1. User taps the attachment button in `ComposeView`. +2. **Image**: Picked via `PhotosPicker` or camera. Compressed to <=255KB. Sent inline with `.image(text, base64Image)` content type. +3. **Video**: Picked from library. Thumbnail generated. Video file sent via XFTP for files >255KB. Content type: `.video(text, thumbnail, duration)`. +4. **File**: Picked via document picker. If <=255KB, sent inline. If >255KB, uploaded via XFTP (up to 1GB). Content type: `.file(text)`. +5. `ComposedMessage` includes `fileSource: CryptoFile(filePath:)`. +6. `apiSendMessages(...)` called with the composed message array. +7. Core handles XFTP upload for large files (chunked, encrypted upload to XFTP servers). +8. Recipient receives file reference and can download. + +### 3. Receive Message + +1. `ChatReceiver.shared` runs `receiveMsgLoop()` continuously calling `chatRecvMsg()`. +2. Core delivers events via `APIResult`. +3. On `ChatEvent.newChatItems(user, chatItems)`: + - `processReceivedMsg` is called. + - For the active user, `ChatModel` is updated with new items. + - If the chat is currently open, `ItemsModel` appends to `reversedChatItems`. + - `NtfManager` posts a local notification if the app is in the background. +4. Small files/images attached to incoming messages are auto-received if within size thresholds. + +### 4. Edit Message + +1. User long-presses a sent message -> selects "Edit" from context menu. +2. `ComposeView` enters edit mode with the original text pre-filled. +3. User modifies text and taps send. +4. Calls `apiUpdateChatItem(type:id:scope:itemId:updatedMessage:live:)`. +5. Dispatches `ChatCommand.apiUpdateChatItem(...)`. +6. Core returns `ChatResponse1.chatItemUpdated(user, aChatItem)` or `.chatItemNotChanged(user, aChatItem)`. +7. `ChatModel` updates the item in place. Edit timestamp is shown in the UI. + +### 5. Delete Message + +1. User long-presses a message -> selects "Delete". +2. Presented with options: + - **Delete for me** (`CIDeleteMode.cidmInternal`) -- removes locally only. + - **Delete for everyone** (`CIDeleteMode.cidmBroadcast`) -- sends deletion to recipient(s). +3. Calls `apiDeleteChatItems(type:id:scope:itemIds:mode:)`. +4. Dispatches `ChatCommand.apiDeleteChatItem(type:id:scope:itemIds:mode:)`. +5. Core returns `ChatResponse1.chatItemsDeleted(user, items, _)` containing `[ChatItemDeletion]`. +6. For group messages from other members, admin/owner can call `apiDeleteMemberChatItems(groupId:itemIds:)`. +7. `ChatModel` removes or replaces items with "deleted" placeholders. + +### 6. React to Message + +1. User long-presses a message -> selects "React" -> picks an emoji. +2. Calls `apiChatItemReaction(type:id:scope:itemId:add:reaction:)`. +3. `reaction` is `MsgReaction` (e.g., `.emoji(.heart)`). +4. `add: true` to add, `add: false` to remove. +5. Core returns `ChatResponse1.chatItemReaction(user, _, reaction)`. +6. The reaction is displayed below the message bubble. + +### 7. Reply to Message + +1. User long-presses a message -> selects "Reply". +2. `ComposeView` enters reply mode, showing quoted message in `ContextItemView`. +3. User types reply text and taps send. +4. `ComposedMessage` is created with `quotedItemId: originalItem.id`. +5. `apiSendMessages(...)` sends with the quote reference. +6. Recipient sees the reply with the quoted context rendered above. + +### 8. Forward Message + +1. User long-presses a message -> selects "Forward". +2. `ChatItemForwardingView` is presented for destination chat selection. +3. `apiPlanForwardChatItems(type:id:scope:itemIds:)` validates what can be forwarded, returns `([Int64], ForwardConfirmation?)`. +4. User confirms and selects destination chat. +5. Calls `apiForwardChatItems(toChatType:toChatId:toScope:fromChatType:fromChatId:fromScope:itemIds:ttl:)`. +6. Core returns `ChatResponse1.newChatItems(...)` with the forwarded items in the destination chat. + +### 9. Voice Message + +1. User taps and holds the microphone button in `ComposeView`. +2. `AudioRecPlay` starts recording to a temporary file. +3. On release, recording stops. Duration is calculated (max 5 minutes / 300 seconds). +4. `ComposedMessage` created with: + - `fileSource: CryptoFile` pointing to the audio file + - `msgContent: .voice(text: "", duration: seconds)` +5. `apiSendMessages(...)` sends the voice message. +6. Voice messages <=510KB sent inline; larger via XFTP. +7. Recipient sees `CIVoiceView` with waveform and playback controls. + +### 10. Delivery Tracking + +1. On send, message status starts as `CIStatus.sndNew`. +2. After SMP delivery: `CIStatus.sndSent(sndProgress)`. +3. When delivered to recipient's agent: status updates to delivered. +4. If delivery receipts are enabled by both parties, read status is reported. +5. Failed delivery results in `CIStatus.sndError*` or `CIStatus.sndWarning*`. +6. Status is displayed via `CIMetaView` (checkmarks/indicators). + +## Data Structures + +| Type | Location | Description | +|------|----------|-------------| +| `ComposedMessage` | `SimpleXChat/APITypes.swift` | Outgoing message: fileSource, quotedItemId, msgContent, mentions | +| `MsgContent` | `SimpleXChat/ChatTypes.swift` | Enum: `.text`, `.link`, `.image`, `.video`, `.voice`, `.file` | +| `CIContent` | `SimpleXChat/ChatTypes.swift` | Chat item content wrapper with sent/received variants | +| `CIStatus` | `SimpleXChat/ChatTypes.swift` | Delivery status: sndNew, sndSent, sndError, rcvNew, rcvRead | +| `CIDirection` | `SimpleXChat/ChatTypes.swift` | `.directSnd`, `.directRcv`, `.groupSnd`, `.groupRcv(groupMember)` | +| `ChatItem` | `SimpleXChat/ChatTypes.swift` | Full message model: content, meta, status, direction, quotedItem | +| `ChatItemDeletion` | `SimpleXChat/ChatTypes.swift` | Deleted item info with old/new item pairs | +| `CIDeleteMode` | `SimpleXChat/ChatTypes.swift` | `.cidmInternal` (local) or `.cidmBroadcast` (for everyone) | +| `MsgReaction` | `SimpleXChat/ChatTypes.swift` | Reaction type (emoji-based) | +| `UpdatedMessage` | `SimpleXChat/APITypes.swift` | Edited message content for update API | + +## Error Cases + +| Error | Cause | Handling | +|-------|-------|----------| +| `ChatError.errorAgent(.SMP(_, .AUTH))` | Recipient queue issue | Show "Connection error (AUTH)" alert | +| `ChatError.errorAgent(.BROKER(_, .TIMEOUT))` | Server timeout | Retryable: show retry dialog via `chatApiSendCmdWithRetry` | +| `ChatError.errorAgent(.BROKER(_, .NETWORK))` | Network failure | Retryable: show retry dialog | +| Send message error | Core processing failure | `sendMessageErrorAlert` shown to user | +| `chatItemNotChanged` | Edit with identical content | No error, item returned unchanged | +| File too large (>1GB) | XFTP limit exceeded | Prevented in UI file picker | +| `fileNotApproved` | Unknown XFTP relay servers | Show "Unknown servers!" alert with approve option | + +## Key Files + +| File | Purpose | +|------|---------| +| `Shared/Views/Chat/ComposeMessage/ComposeView.swift` | Message composition UI and send logic | +| `Shared/Views/Chat/ComposeMessage/SendMessageView.swift` | Text input and send button | +| `Shared/Views/Chat/ComposeMessage/ContextItemView.swift` | Reply/edit context display | +| `Shared/Views/Chat/ChatItemView.swift` | Per-message rendering dispatcher | +| `Shared/Views/Chat/ChatItem/MsgContentView.swift` | Text message content with markdown | +| `Shared/Views/Chat/ChatItem/CIMetaView.swift` | Delivery status indicators | +| `Shared/Views/Chat/ChatItemForwardingView.swift` | Forward destination picker | +| `Shared/Views/Chat/ChatItemInfoView.swift` | Message info (delivery details, timestamps) | +| `Shared/Model/SimpleXAPI.swift` | API functions: `apiSendMessages`, `apiUpdateChatItem`, `apiDeleteChatItems`, `apiChatItemReaction`, `apiForwardChatItems` | +| `SimpleXChat/APITypes.swift` | `ComposedMessage`, `ChatCommand` enum, response types | +| `SimpleXChat/ChatTypes.swift` | `MsgContent`, `CIContent`, `CIStatus`, `CIDirection`, `ChatItem` | +| `Shared/Model/AudioRecPlay.swift` | Voice message recording/playback engine | + +## Related Specifications + +- `apps/ios/product/views/chat.md` -- Chat view UI specification +- `apps/ios/product/README.md` -- Product overview and capability map diff --git a/apps/ios/product/flows/onboarding.md b/apps/ios/product/flows/onboarding.md new file mode 100644 index 0000000000..5e2e04d42a --- /dev/null +++ b/apps/ios/product/flows/onboarding.md @@ -0,0 +1,239 @@ +# Onboarding Flow + +> **Related spec:** [spec/architecture.md](../../spec/architecture.md) | [spec/database.md](../../spec/database.md) + +## Overview + +First-time setup and migration flows for SimpleX Chat iOS. Covers app initialization, profile creation, server operator selection, notification configuration, and database import/export for device migration. The app uses a Haskell runtime for its core chat engine, with SQLite databases shared between the main app and the Notification Service Extension (NSE). + +## Prerequisites + +- Fresh install of SimpleX Chat from the App Store, or +- Existing install with database archive for import/migration +- iOS 15+ with App Group entitlement configured + +## Step-by-Step Processes + +### 1. App Initialization Sequence + +On every app launch, `SimpleXApp.init()` executes the following in order: + +``` +1. haskell_init() -- Start Haskell runtime system (GHC RTS) +2. UserDefaults.standard.register(defaults:) -- Set default preferences (appDefaults) +3. setGroupDefaults() -- Configure app group shared defaults +4. registerGroupDefaults() -- Register group container defaults +5. setDbContainer() -- Configure database paths in app group container +6. BGManager.shared.register() -- Register background task handlers +7. NtfManager.shared.registerCategories() -- Register notification action categories +``` + +Then in `ContentView.onAppear`: +- If no migration is in progress and authentication is set up, `initChatAndMigrate()` is called. +- This triggers `chatMigrateInit()` to initialize/migrate databases. +- Then `startChat()` is called to start the chat engine. + +### 2. Fresh Install -- Onboarding Steps + +Onboarding is managed by `OnboardingStage` enum and `OnboardingView`: + +**Step 1: SimpleX Info** (`step1_SimpleXInfo`) +1. `SimpleXInfo` view is presented. +2. Explains SimpleX's architecture: no user identifiers, E2E encryption, decentralized servers. +3. User taps "Create your profile" to proceed. + +**Step 2: Create Profile** (`step2_CreateProfile` -- now inline in step 1) +1. `CreateFirstProfile` view (embedded in the onboarding flow). +2. User enters display name (required). Full name is set to empty string. +3. Display name is validated via `mkValidName()` and `canCreateProfile()`. +4. On "Create": + ```swift + AppChatState.shared.set(.active) + m.currentUser = try apiCreateActiveUser(profile) + try startChat() + ``` +5. `apiCreateActiveUser(Profile(displayName:fullName:shortDescr:))` creates the user in the Haskell core. +6. `startChat()` initializes the chat engine. +7. Onboarding advances to `step3_ChooseServerOperators`. + +**Step 3: Choose Server Operators** (`step3_ChooseServerOperators`) +1. `OnboardingConditionsView` is presented (simplified conditions acceptance). +2. User reviews and accepts server operator conditions. +3. This configures which SMP/XFTP server operators to use. +4. Advances to `step4_SetNotificationsMode`. + +**Step 4: Set Notifications** (`step4_SetNotificationsMode`) +1. `SetNotificationsMode` view is presented. +2. Three options: + - **Instant**: Requires Apple Push Notification service. Registers device token via `apiRegisterToken(token:notificationMode:)`. + - **Periodic**: Uses iOS background app refresh. No push token needed. + - **Off**: No notifications. +3. For instant mode: `apiRegisterToken` sends `ChatCommand.apiRegisterToken(token:notificationMode:)` and receives `ChatResponse2.ntfTokenStatus(status)`. +4. On completion: `onboardingStageDefault.set(.onboardingComplete)`. + +**Onboarding Complete** (`onboardingComplete`) +1. `ChatListView` is shown. +2. Empty state displays "Add contact" prompt via `ChatHelp`. +3. If delivery receipts haven't been configured: `chatModel.setDeliveryReceipts = true` triggers a prompt. + +### 3. startChat() -- Chat Engine Startup + +Called after profile creation or on subsequent app launches: + +```swift +func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws { + 1. setNetworkConfig(getNetCfg()) -- Apply network configuration + 2. apiCheckChatRunning() -- Check if already running + 3. listUsers() -- Load all user profiles + 4. getUserChatData() -- Load chats, tags, address, TTL + 5. NtfManager.shared.setNtfBadgeCount(...) -- Set badge count + 6. refreshCallInvitations() -- Check pending call invitations + 7. apiGetNtfToken() -- Get notification token status + 8. apiStartChat() -- Start the Haskell chat engine + 9. registerToken(token:) -- Register push token if available + 10. ChatReceiver.shared.start() -- Start message receive loop +} +``` + +### 4. Database Setup + +**Location:** +- App group container (shared with NSE): determined by `dbContainerGroupDefault` +- Path prefix: `simplex_v1` (`DB_FILE_PREFIX`) +- Chat database: `simplex_v1_chat.db` (messages, contacts, groups, settings) +- Agent database: `simplex_v1_agent.db` (SMP connections, encryption keys, queues) + +**Initialization:** +- `chatMigrateInit(useKey:confirmMigrations:backgroundMode:)` in `SimpleXChat/API.swift`. +- Creates databases if they do not exist. +- Runs pending migrations with confirmation mode. +- Handles database encryption: + - If keychain storage enabled: generates random DB key on first run (`randomDatabasePassword()`). + - Stores key in keychain via `kcDatabasePassword`. + - `initialRandomDBPassphraseGroupDefault` tracks whether using auto-generated key. + +**Encryption:** +- Optional database encryption passphrase via `DatabaseEncryptionView`. +- `apiStorageEncryption(currentKey:newKey:)` changes encryption key. +- `testStorageEncryption(key:)` validates a key against the database. + +### 5. Database Export (Source Device) + +1. User navigates to Settings -> Database -> "Export database". +2. Chat must be stopped first for data consistency. +3. Calls `apiExportArchive(config: ArchiveConfig)`: + ```swift + func apiExportArchive(config: ArchiveConfig) async throws -> [ArchiveError] + ``` +4. Core creates a ZIP archive containing both databases and file attachments. +5. Returns any non-fatal `[ArchiveError]` (e.g., file access issues). +6. User transfers the archive to the new device via AirDrop, file share, etc. + +### 6. Database Import (Destination Device) + +1. On new device: during onboarding or Settings -> Database -> "Import database". +2. User selects the archive file. +3. Calls `apiImportArchive(config: ArchiveConfig)`: + ```swift + func apiImportArchive(config: ArchiveConfig) async throws -> [ArchiveError] + ``` +4. Core extracts the archive, replacing local databases. +5. Returns any non-fatal `[ArchiveError]`. +6. Chat engine is restarted with the imported data. +7. All contacts, groups, messages, and settings are restored. + +### 7. In-App Device Migration + +An alternative to manual export/import using direct device-to-device transfer. + +**Source device** (`MigrateFromDevice` view): +1. User navigates to Settings -> Database -> "Migrate to another device". +2. App creates a temporary database and uploads archive via XFTP standalone file. +3. Generates a migration link containing the file URL and encryption key. +4. Displays QR code / share link for the destination device. + +**Destination device** (`MigrateToDevice` view): +1. On new device: onboarding detects migration state or user selects "Migrate". +2. Scans/pastes the migration link. +3. `downloadStandaloneFile(user:url:file:ctrl:)` downloads the archive from XFTP. +4. `standaloneFileInfo(url:ctrl:)` validates the file metadata. +5. Archive is imported, databases are restored. +6. `chatInitTemporaryDatabase(url:key:confirmation:)` may be used for temporary DB operations during migration. +7. Chat engine starts with the migrated data. + +If migration is interrupted: +- `chatModel.migrationState` preserves state across app restarts. +- On next launch, `ContentView.onAppear` detects pending migration and resumes. + +### 8. Additional Profile Creation (Multi-Account) + +1. From `UserPicker` (profile switcher) -> "Add profile". +2. `CreateProfile` view is presented (distinct from `CreateFirstProfile`). +3. User enters display name and optional bio (max 160 bytes JSON-encoded, `MAX_BIO_LENGTH_BYTES`). +4. `apiCreateActiveUser(profile)` creates additional user. +5. `listUsers()` and `getUserChatData()` refresh the model. +6. No onboarding steps -- goes directly to chat list. + +## Data Structures + +| Type | Location | Description | +|------|----------|-------------| +| `OnboardingStage` | `Shared/Views/Onboarding/OnboardingView.swift` | Enum: `step1_SimpleXInfo`, `step2_CreateProfile`, `step3_ChooseServerOperators`, `step4_SetNotificationsMode`, `onboardingComplete` | +| `Profile` | `SimpleXChat/ChatTypes.swift` | `displayName`, `fullName`, `image`, `shortDescr` | +| `User` | `SimpleXChat/ChatTypes.swift` | Full user model with profile, userId, and settings | +| `ArchiveConfig` | `SimpleXChat/APITypes.swift` | Configuration for database export/import | +| `DBMigrationResult` | `SimpleXChat/API.swift` | Result of database migration: `.ok`, `.errorNotADatabase`, `.errorKeychain`, etc. | +| `MigrationConfirmation` | `SimpleXChat/API.swift` | Migration confirmation mode: `.error`, `.yesUp`, `.yesUpDown` | +| `DeviceToken` | `SimpleXChat/ChatTypes.swift` | Apple push notification device token | +| `NtfTknStatus` | `SimpleXChat/ChatTypes.swift` | Notification token status: registered, active, expired, etc. | +| `NotificationsMode` | `SimpleXChat/ChatTypes.swift` | `.off`, `.periodic`, `.instant` | +| `MigrationFileLinkData` | Used in standalone file transfers for device migration | +| `AppChatState` | `SimpleXChat/` | Shared state: `.active`, `.stopped`, `.suspended` | + +## Error Cases + +| Error | Cause | Handling | +|-------|-------|----------| +| `DBMigrationResult.errorNotADatabase` | Wrong encryption key or corrupt DB | Show `DatabaseErrorView` with options | +| `DBMigrationResult.errorKeychain` | Keychain access failed | Show error, offer to re-enter passphrase | +| `DBMigrationResult.errorMigration` | Schema migration failure | Show error with migration details | +| `duplicateUserError` | Display name already in use | `UserProfileAlert.duplicateUserError` | +| `invalidDisplayNameError` | Invalid characters in display name | `UserProfileAlert.invalidDisplayNameError` | +| `createUserError` | Core failed to create user | Alert with error details | +| `invalidNameError(validName)` | Name needs normalization | Alert suggesting the valid name | +| Archive import errors | Missing files, version mismatch | Non-fatal `[ArchiveError]` displayed | +| Migration interrupted | Network failure, app killed | State preserved in `chatModel.migrationState`, resumed on next launch | + +## Key Files + +| File | Purpose | +|------|---------| +| `Shared/SimpleXApp.swift` | App entry point: `haskell_init`, defaults registration, DB container setup, BG tasks | +| `Shared/AppDelegate.swift` | Push notification registration, URL handling | +| `Shared/ContentView.swift` | Root view: authentication, onboarding routing, chat initialization | +| `Shared/Views/Onboarding/OnboardingView.swift` | Onboarding step router, `OnboardingStage` enum | +| `Shared/Views/Onboarding/SimpleXInfo.swift` | Step 1: Privacy architecture explanation | +| `Shared/Views/Onboarding/CreateProfile.swift` | Profile creation: `CreateProfile` (additional) and `CreateFirstProfile` (onboarding) | +| `Shared/Views/Onboarding/ChooseServerOperators.swift` | Step 3: Server operator conditions | +| `Shared/Views/Onboarding/SetNotificationsMode.swift` | Step 4: Notification mode selection | +| `Shared/Views/Onboarding/CreateSimpleXAddress.swift` | Optional address creation during onboarding | +| `Shared/Views/Onboarding/HowItWorks.swift` | Educational content about SimpleX protocol | +| `Shared/Views/Migration/MigrateFromDevice.swift` | Source device migration UI | +| `Shared/Views/Migration/MigrateToDevice.swift` | Destination device migration UI | +| `Shared/Views/Database/DatabaseView.swift` | Database management: export, import, encryption | +| `Shared/Views/Database/DatabaseEncryptionView.swift` | Database passphrase management | +| `Shared/Views/Database/DatabaseErrorView.swift` | Database error recovery UI | +| `Shared/Views/Database/MigrateToAppGroupView.swift` | Legacy migration from Documents to App Group container | +| `Shared/Model/SimpleXAPI.swift` | `startChat`, `apiCreateActiveUser`, `apiExportArchive`, `apiImportArchive`, `apiRegisterToken` | +| `SimpleXChat/API.swift` | `chatMigrateInit`, `chatInitTemporaryDatabase`, low-level DB initialization | +| `SimpleXChat/FileUtils.swift` | DB file paths, constants (`DB_FILE_PREFIX`, `CHAT_DB`, `AGENT_DB`) | +| `SimpleXChat/AppGroup.swift` | App group container configuration | +| `SimpleXChat/KeyChain.swift` | Keychain access for DB passphrase and app passwords | +| `Shared/Model/BGManager.swift` | Background task registration and scheduling | +| `Shared/Model/NtfManager.swift` | Notification management and badge counts | + +## Related Specifications + +- `apps/ios/product/README.md` -- Product overview: architecture and capabilities +- `apps/ios/product/flows/connection.md` -- After onboarding, user establishes first connections +- `apps/ios/product/flows/messaging.md` -- Messaging starts after profile creation diff --git a/apps/ios/product/gaps.md b/apps/ios/product/gaps.md new file mode 100644 index 0000000000..04cf97a6a7 --- /dev/null +++ b/apps/ios/product/gaps.md @@ -0,0 +1,61 @@ +# SimpleX Chat iOS -- Known Gaps & Recommendations + +> Aggregation of `[GAP]` and `[REC]` annotations discovered during specification analysis. Organized by product area. +> +> **Related spec:** [spec/README.md](../spec/README.md) + +--- + +## UI: Error Feedback + +### GAP: No user-visible error on FFI command failure +**Source:** [spec/architecture.md](../spec/architecture.md) +API calls via `chatApiSendCmd` return `APIResult` which can be `.error(ChatError)`. Not all error cases surface user-visible feedback in the UI. + +**REC:** Audit all `chatApiSendCmd` call sites and ensure `.error` cases show appropriate alerts or banners. + +--- + +## UI: Loading States + +### GAP: No loading indicator during initial chat list population +**Source:** [spec/client/chat-list.md](../spec/client/chat-list.md) +When `ChatModel.chatInitialized` transitions to `true`, the chat list appears fully formed. There is no intermediate loading state for users with large numbers of chats. + +**REC:** Add a progress indicator during `apiGetChats` for users with 100+ conversations. + +--- + +## Flows: Group Lifecycle + +### GAP: Bulk member role change — API supports batch but UI uses single-member calls +**Source:** [spec/api.md](../spec/api.md) +`APIMembersRole` accepts `NonEmpty GroupMemberId`, supporting batch role changes at the API level. However, the iOS UI (`GroupMemberInfoView.swift`) currently invokes it with a single member at a time. + +**REC:** Expose batch role change in the UI for group admins managing large groups. + +--- + +## Security + +### GAP: Database passphrase not enforced by default +**Source:** [spec/database.md](../spec/database.md) +Database encryption is optional and requires the user to manually set a passphrase. New installations start with an unencrypted database. + +**REC:** Consider prompting users to set a database passphrase during onboarding, especially on devices without hardware encryption. + +### GAP: No forward secrecy indicator in UI +**Source:** [product/glossary.md](glossary.md) +While the double-ratchet protocol provides forward secrecy, there is no UI indicator showing whether a specific conversation has achieved forward secrecy (i.e., completed initial key exchange ratcheting). + +**REC:** Add a security indicator in contact/group info showing ratchet state. + +--- + +## Documentation + +### GAP: Haskell Store layer not fully specified +**Source:** [spec/database.md](../spec/database.md) +The Haskell Store modules (`Store/Direct.hs`, `Store/Groups.hs`, `Store/Messages.hs`, etc.) are referenced by function name but not fully specified with parameter types and return types. + +**REC:** Expand database spec with key Store function signatures as the specification matures. diff --git a/apps/ios/product/glossary.md b/apps/ios/product/glossary.md new file mode 100644 index 0000000000..0353c8f606 --- /dev/null +++ b/apps/ios/product/glossary.md @@ -0,0 +1,235 @@ +# SimpleX Chat iOS -- Glossary + +> SimpleX Chat iOS domain glossary. Defines all domain terms used in SimpleX Chat with links to relevant specifications and source code. +> +> **Related spec:** [spec/api.md](../spec/api.md) | [spec/architecture.md](../spec/architecture.md) + +## Table of Contents + +1. [Protocols & Cryptography](#protocols--cryptography) +2. [Core Data Types](#core-data-types) +3. [Commands & Events](#commands--events) +4. [Connection & Identity](#connection--identity) +5. [Messaging Features](#messaging-features) +6. [Calling & Media](#calling--media) +7. [Notifications & Background](#notifications--background) +8. [Application Architecture](#application-architecture) +9. [Configuration & Preferences](#configuration--preferences) + +--- + +## Protocols & Cryptography + +### SMP (Simplex Messaging Protocol) +The core messaging protocol used for asynchronous message delivery through relay servers. Each conversation uses separate unidirectional queues, and sender and receiver queues have no shared identifier. Defined in the [simplexmq](https://github.com/simplex-chat/simplexmq) library. *See: protocol spec `simplexmq/protocol/simplex-messaging.md`, implementation `simplexmq/src/Simplex/Messaging/Protocol.hs`* + +### SMP Server +A relay server that stores and forwards encrypted messages between parties. Users can configure custom SMP servers or use defaults. Servers cannot see message contents or correlate senders with receivers. *See: `Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift`* + +### XFTP (eXtended File Transfer Protocol) +A protocol for transferring large files (up to 1GB) through relay servers. Files are encrypted, split into chunks, and uploaded to XFTP servers. Recipients download and reassemble chunks independently. Defined in the [simplexmq](https://github.com/simplex-chat/simplexmq) library. *See: protocol spec `simplexmq/protocol/xftp.md`, implementation `simplexmq/src/Simplex/FileTransfer/Protocol.hs`; chat-level integration `../../src/Simplex/Chat/Files.hs`* + +### XFTP Server +A relay server that stores encrypted file chunks for asynchronous file transfer. Like SMP servers, users can configure custom XFTP servers. *See: `Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift`* + +### SMP Agent +The lower-level agent library (in [simplexmq](https://github.com/simplex-chat/simplexmq)) that manages SMP connections, queue creation/rotation, duplex connection establishment, message delivery, and the double-ratchet encryption protocol. The chat application layer communicates with the agent via its functional API. *See: protocol spec `simplexmq/protocol/agent-protocol.md`, implementation `simplexmq/src/Simplex/Messaging/Agent.hs`; chat-level integration `../../src/Simplex/Chat/Controller.hs`* + +### Double Ratchet +The key agreement protocol used for E2E encryption. Provides forward secrecy and break-in recovery by deriving new encryption keys for each message. Based on the Signal protocol's double-ratchet algorithm, augmented with post-quantum KEM (PQDR). Implemented in the [simplexmq](https://github.com/simplex-chat/simplexmq) library. *See: protocol spec `simplexmq/protocol/pqdr.md`, implementation `simplexmq/src/Simplex/Messaging/Crypto/Ratchet.hs`* + +### Post-Quantum Encryption +Optional quantum-resistant key exchange (PQ) available for direct chats. Uses a hybrid scheme combining classical X25519 with Streamlined NTRU-Prime 761 (sntrup761) KEM. The hybrid secret is SHA3-256(DH_secret || KEM_shared_secret). Implemented in the [simplexmq](https://github.com/simplex-chat/simplexmq) library. *See: protocol spec `simplexmq/protocol/pqdr.md`, implementation `simplexmq/src/Simplex/Messaging/Crypto/SNTRUP761.hs`; Swift types `SimpleXChat/ChatTypes.swift` (PQEncryption, PQSupport)* + +### E2E Encryption +End-to-end encryption ensuring that only the communicating parties can read message contents. Neither SMP relay servers nor any network observer can decrypt messages. All SimpleX Chat messages are E2E encrypted by default using the double-ratchet protocol. *See: `simplexmq/src/Simplex/Messaging/Crypto/Ratchet.hs` (ratchet implementation), `simplexmq/src/Simplex/Messaging/Agent/Protocol.hs` (E2E message envelopes)* + +### Forward Secrecy +A property of the double-ratchet protocol ensuring that compromise of current encryption keys does not compromise past session keys. Each message uses a derived key that is deleted after use. *See: `simplexmq/protocol/pqdr.md`, `simplexmq/src/Simplex/Messaging/Crypto/Ratchet.hs`* + +### Chat Protocol (x-events) +The chat-level protocol defining message envelopes and content types exchanged between chat participants. Includes x-events (XMsgNew, XMsgUpdate, XMsgDel, XCallInv, XFileCancel, XGrpMemNew, etc.), MsgContent (text, image, video, voice, file, link), and message encoding (Binary/JSON). This is distinct from the lower-level SMP transport protocol. *See: `../../src/Simplex/Chat/Protocol.hs`* + +### Security Code +A hash of the shared encryption session displayed as a numeric code and QR code. Contacts can compare security codes out-of-band to verify they have an uncompromised E2E session. *See: `Shared/Views/Chat/VerifyCodeView.swift`, `../../src/Simplex/Chat/Controller.hs` (APIVerifyContact)* + +--- + +## Core Data Types + +### ChatItem +The fundamental unit of content in a conversation. Represents a single message, event, call record, or system notification within a chat. Each ChatItem has direction (sent/received), content, metadata, and optional quoted context. *See: `../../src/Simplex/Chat/Messages.hs` (data ChatItem), `SimpleXChat/ChatTypes.swift`* + +### ChatInfo +A type-safe wrapper identifying a conversation and its metadata. Variants: DirectChat (1:1 with Contact), GroupChat (with GroupInfo), LocalChat (note folder), ContactRequest, ContactConnection. *See: `../../src/Simplex/Chat/Messages.hs` (data ChatInfo), `SimpleXChat/ChatTypes.swift`* + +### CIContent +The content payload of a ChatItem. Differentiates sent vs. received content types: message content (text/image/file/voice/link), deletion markers, call records, group events, and feature preference changes. *See: `../../src/Simplex/Chat/Messages/CIContent.hs` (data CIContent)* + +### User +A local user profile within the app. Each user has an independent set of contacts, groups, and connections. Multiple users can exist in one app installation. Fields include userId, profile, display name, and optional view password hash for hidden profiles. *See: `../../src/Simplex/Chat/Types.hs` (data User), `Shared/Model/ChatModel.swift`* + +### Contact +A remote party with whom the user has an established E2E encrypted connection. Stores the contact's profile, local alias, connection status, feature preferences, and UI settings. *See: `../../src/Simplex/Chat/Types.hs` (data Contact), `SimpleXChat/ChatTypes.swift`* + +### GroupInfo +Metadata for a group conversation including group profile, member count, preferences, and membership status. Contains the user's own membership record as a GroupMember. *See: `../../src/Simplex/Chat/Types.hs` (data GroupInfo)* + +### GroupMember +A participant in a group conversation. Each member has a role, status, profile, and optionally a direct connection. The user's own membership is also represented as a GroupMember within GroupInfo. *See: `../../src/Simplex/Chat/Types.hs` (data GroupMember)* + +### Connection +A low-level SMP agent connection between two parties. Each connection has a status (new, joined, ready, deleted), an agent connection ID, and is associated with a specific contact or group member. *See: `../../src/Simplex/Chat/Types.hs` (data Connection)* + +### ConnStatus +The lifecycle state of a Connection: ConnNew (created, awaiting join), ConnJoined (joined, handshake in progress), ConnReady (fully established), ConnDeleted (terminated). *See: `../../src/Simplex/Chat/Types.hs` (data ConnStatus)* + +### ContactStatus +The status of a contact record: CSActive (normal), CSDeleted (deleted by contact), CSDeletedByUser (deleted by user). *See: `../../src/Simplex/Chat/Types.hs` (data ContactStatus)* + +### GroupMemberRole +Hierarchical role assigned to a group member. From most to least privileged: GROwner, GRAdmin, GRModerator, GRMember, GRObserver. Roles determine permissions for sending messages, managing members, and moderating content. *See: `../../src/Simplex/Chat/Types/Shared.hs` (data GroupMemberRole)* + +### GroupMemberStatus +The lifecycle state of a group member: GSMemRejected, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown, GSMemInvited, GSMemIntroduced, GSMemIntroInvited, GSMemAccepted, GSMemAnnounced, GSMemConnected, GSMemComplete, GSMemCreator, GSMemPendingReview, GSMemPendingApproval. *See: `../../src/Simplex/Chat/Types.hs` (data GroupMemberStatus)* + +### FileTransfer +Represents an in-progress or completed file transfer. Variants: FTSnd (sending, with metadata and per-recipient transfer records) and FTRcv (receiving). Tracks protocol (SMP inline or XFTP), progress, and encryption parameters. *See: `../../src/Simplex/Chat/Types.hs` (data FileTransfer)* + +### ChatTag +A user-defined label for organizing conversations in the chat list. Each tag has a text label and optional emoji. Chats can have multiple tags, and the chat list can be filtered by tag. *See: `../../src/Simplex/Chat/Types.hs` (data ChatTag), `Shared/Views/ChatList/TagListView.swift`* + +--- + +## Commands & Events + +### ChatCommand +A sum type representing all commands the UI can send to the chat controller. Examples: APISendMessages, APIGetChat, APIConnect, APINewGroup, APIDeleteChatItem. Commands are serialized and dispatched through the FFI bridge. *See: `../../src/Simplex/Chat/Controller.hs` (data ChatCommand)* + +### ChatResponse +A sum type representing synchronous responses from the chat controller to the UI after processing a ChatCommand. Examples: CRActiveUser, CRNewChatItems, CRChatItemUpdated. *See: `../../src/Simplex/Chat/Controller.hs` (data ChatResponse)* + +### ChatEvent +A sum type representing asynchronous events pushed from the chat controller to the UI. These are unsolicited notifications about state changes: incoming messages, connection status changes, call invitations, etc. *See: `../../src/Simplex/Chat/Controller.hs` (data ChatEvent)* + +### ChatError +Error types returned by the chat controller. Variants: ChatError (application-level), ChatErrorAgent (SMP agent errors), ChatErrorStore (database errors), ChatErrorRemoteHost (remote desktop errors). *See: `../../src/Simplex/Chat/Controller.hs` (data ChatError)* + +--- + +## Connection & Identity + +### SimpleX Address +A long-lived contact address that others can use to send connection requests. Unlike one-time invitation links, an address can be reused by multiple contacts. The user can accept or reject each incoming request. *See: `Shared/Views/UserSettings/UserAddressView.swift`, `../../src/Simplex/Chat/Controller.hs` (APICreateMyAddress)* + +### Contact Link +A one-time or reusable URI that initiates a contact connection. When scanned or opened, it triggers the SMP handshake to establish an E2E encrypted channel between two parties. *See: `Shared/Views/NewChat/NewChatView.swift`* + +### Group Link +A shareable URI that allows new members to join a group. The link connects to the group host, who then introduces the new member to existing members. Configurable with a default member role. *See: `Shared/Views/Chat/Group/GroupLinkView.swift`, `../../src/Simplex/Chat/Types.hs` (data GroupLink)* + +### Short Link +A compact version of SimpleX contact or group links, using a shorter URI format for easier sharing. Contains encoded connection parameters with reduced character length. *See: `../../src/Simplex/Chat/Controller.hs`* + +### Incognito Mode +A privacy feature that generates a random profile (display name and avatar) for each new contact connection. The real user profile is never shared with incognito contacts. Can be toggled per-connection at invitation time. *See: `Shared/Views/UserSettings/IncognitoHelp.swift`, `../../src/Simplex/Chat/ProfileGenerator.hs`* + +### Hidden Profile +A user profile protected by a separate password. Hidden profiles do not appear in the user picker or profile list. To access a hidden profile, the user enters its password in the search field of the user picker. *See: `Shared/Views/UserSettings/HiddenProfileView.swift`, `../../src/Simplex/Chat/Controller.hs` (APIHideUser)* + +--- + +## Messaging Features + +### Delivery Receipt +A confirmation that a message was successfully delivered to the recipient's device. Displayed as a double-check indicator on sent messages. Can be enabled or disabled per contact or globally. *See: `Shared/Views/UserSettings/SetDeliveryReceiptsView.swift`, `../../src/Simplex/Chat/Controller.hs`* + +### Read Receipt +An indicator that a recipient has viewed a received message. Currently not implemented as a separate feature; delivery receipts serve as the primary delivery confirmation. *See: `Shared/Views/UserSettings/PrivacySettings.swift`* + +### Timed Message +A message with a configurable time-to-live (TTL). After the TTL expires, the message is automatically deleted from both sender and recipient devices. The TTL is set as a chat feature preference. Also referred to as a disappearing message. *See: `../../src/Simplex/Chat/Types/Preferences.hs` (TimedMessagesPreference)* + +### Disappearing Message +Synonym for Timed Message. A message that self-destructs after a configured duration. The timer starts when the message is read by the recipient. *See: `../../src/Simplex/Chat/Types/Preferences.hs` (TimedMessagesPreference)* + +### Message Integrity +Verification that messages are received in order and without gaps. The system detects skipped messages and decryption failures, displaying integrity error indicators in the chat. *See: `Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift`, `../../src/Simplex/Chat/Messages/CIContent.hs`* + +### Decryption Error +An error occurring when a received message cannot be decrypted, typically due to ratchet synchronization issues. The UI displays a specific error view with recovery options. *See: `Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift`, `../../src/Simplex/Chat/Messages/CIContent.hs`* + +--- + +## Calling & Media + +### CallKit +Apple's framework for integrating VoIP calls with the native iOS call UI. SimpleX Chat uses CallKit to display incoming calls on the lock screen, support call answering from the system UI, and manage audio sessions. *See: `Shared/Views/Call/CallController.swift`, `Shared/Views/Call/CallManager.swift`* + +### WebRTC +The real-time communication framework used for audio/video calls. SimpleX Chat wraps WebRTC in an E2E encrypted layer, with signaling performed through the existing SMP message channel rather than a central server. *See: `Shared/Views/Call/WebRTC.swift`, `Shared/Views/Call/WebRTCClient.swift`* + +### ICE Server +An Interactive Connectivity Establishment server used by WebRTC to discover network paths between call participants. SimpleX Chat supports configuring custom ICE servers. *See: `Shared/Views/UserSettings/RTCServers.swift`, `SimpleXChat/CallTypes.swift`* + +### TURN Server +A Traversal Using Relays around NAT server that relays WebRTC media when direct peer-to-peer connection is not possible. A specific type of ICE server. SimpleX Chat allows configuring custom TURN servers for call relay. *See: `Shared/Views/UserSettings/RTCServers.swift`* + +### RcvCallInvitation +An in-memory data structure representing an incoming call invitation. Contains the calling contact, call type (audio/video), encryption keys, and shared key for the WebRTC session. Not persisted to database. *See: `../../src/Simplex/Chat/Call.hs` (data RcvCallInvitation)* + +--- + +## Notifications & Background + +### Notification Service Extension (NSE) +An iOS app extension that processes incoming push notifications while the main app is not running. The NSE starts a temporary chat controller, decrypts the incoming message, and displays a notification with the message preview. *See: `SimpleX NSE/NotificationService.swift`, `SimpleX NSE/NSEAPITypes.swift`* + +### Background Task +An iOS background execution context used for periodic message fetching when instant notifications are not enabled. Managed by BGManager to check for new messages at system-determined intervals. *See: `Shared/Model/BGManager.swift`* + +--- + +## Application Architecture + +### chat_ctrl +The opaque C pointer to the Haskell chat controller, obtained via FFI initialization. All chat operations are dispatched through this controller handle. The main app and NSE maintain separate chat_ctrl instances. *See: `SimpleXChat/API.swift` (chatController, getChatCtrl)* + +### ComposeState +A Swift struct holding the current state of the message composition area. Tracks the message text, parsed markdown, preview, attached media, editing context, quote context, and voice recording state. *See: `Shared/Views/Chat/ComposeMessage/ComposeView.swift` (struct ComposeState)* + +### ChatModel +The central observable model object for the iOS app. Holds all reactive state: current user, chat list, active chat, call state, app preferences, and navigation state. Published properties drive SwiftUI view updates. *See: `Shared/Model/ChatModel.swift` (class ChatModel)* + +### ItemsModel +An observable model managing the list of ChatItems displayed in a conversation view. Handles item loading, pagination, merging of new items, and secondary chat filtering. *See: `Shared/Model/ChatModel.swift` (class ItemsModel)* + +### AppTheme +An observable object encapsulating the current visual theme: name, base theme, color overrides, app-specific colors, and wallpaper configuration. Shared as an environment object across the SwiftUI view hierarchy. *See: `Shared/Theme/Theme.swift` (class AppTheme)* + +--- + +## Configuration & Preferences + +### FeaturePreference +A type class (Haskell) / protocol pattern representing a user's preference for a specific chat feature (e.g., timed messages, voice messages, calls). Each preference has an allow/enable setting and optional parameters. Feature preferences are negotiated between contacts. *See: `../../src/Simplex/Chat/Types/Preferences.hs` (class FeatureI, type FeaturePreference)* + +### ChatSettings +Per-chat configuration including notification mode (all/mentions/off), send receipts toggle, favorite flag, and tag assignments. Stored per contact and per group. *See: `../../src/Simplex/Chat/Types.hs` (data ChatSettings)* + +### UserDefaults / GroupDefaults +iOS persistent key-value storage for app preferences. GroupDefaults (UserDefaults with the app group suite name) is shared between the main app and the NSE extension. Stores settings like notification mode, appearance preferences, and runtime flags. *See: `SimpleXChat/AppGroup.swift` (groupDefaults)* + +--- + +## Cross-References + +- Product overview: [README.md](README.md) +- Concept index: [concepts.md](concepts.md) +- Haskell core types: `../../src/Simplex/Chat/Types.hs` +- Haskell controller: `../../src/Simplex/Chat/Controller.hs` +- Haskell chat protocol (x-events): `../../src/Simplex/Chat/Protocol.hs` +- Haskell messages: `../../src/Simplex/Chat/Messages.hs` +- Swift model: `Shared/Model/ChatModel.swift` +- Swift API types: `SimpleXChat/APITypes.swift`, `SimpleXChat/ChatTypes.swift` +- simplexmq library (SMP, XFTP, Agent, encryption): [github.com/simplex-chat/simplexmq](https://github.com/simplex-chat/simplexmq) diff --git a/apps/ios/product/rules.md b/apps/ios/product/rules.md new file mode 100644 index 0000000000..b41792898b --- /dev/null +++ b/apps/ios/product/rules.md @@ -0,0 +1,119 @@ +# SimpleX Chat iOS -- Business Rules + +> Business invariants enforced by the SimpleX Chat iOS app and Haskell core. Each rule states the invariant, where it is enforced, and links to the relevant spec. +> +> **Related spec:** [spec/api.md](../spec/api.md) | [spec/architecture.md](../spec/architecture.md) | [spec/state.md](../spec/state.md) + +--- + +## Security & Privacy + +### RULE-01: No user identifiers +**Rule:** The system MUST NOT assign, generate, or expose any persistent user identifier (phone number, email, username, UUID) that could be used to correlate a user across conversations. +**Enforced by:** SMP protocol design in simplexmq library; each connection uses independent unidirectional queues with no shared identifier. +**Spec:** [spec/architecture.md](../spec/architecture.md) + +### RULE-02: End-to-end encryption on all messages +**Rule:** All message content MUST be encrypted end-to-end using double-ratchet (with optional post-quantum KEM). The SMP server MUST NOT have access to plaintext. +**Enforced by:** simplexmq library (`Simplex.Messaging.Crypto.Ratchet`); encryption happens before `chat_send_cmd_retry` FFI call. +**Spec:** [spec/architecture.md](../spec/architecture.md) + +### RULE-03: Database encryption at rest +**Rule:** Both SQLite databases (chat and agent) MUST be encrypted with SQLCipher when the user sets a database passphrase. +**Enforced by:** `chat_migrate_init_key` in Haskell core via SQLCipher; `DatabaseEncryptionView.swift` in UI. +**Spec:** [spec/database.md](../spec/database.md) + +### RULE-04: Local authentication before content access +**Rule:** When app lock is enabled, the app MUST authenticate the user (Face ID, Touch ID, or passcode) before displaying any chat content. +**Enforced by:** `LocalAuthView.swift`, `ContentView.swift` (`contentViewAccessAuthenticated` guard on `ChatModel`). +**Spec:** [spec/architecture.md](../spec/architecture.md) + +### RULE-05: Incognito profiles are per-connection +**Rule:** When incognito mode is used for a connection, the generated random profile MUST be unique to that connection and MUST NOT be reused across connections. +**Enforced by:** `ProfileGenerator.hs` generates fresh profile per connection; stored on the connection entity. +**Spec:** [spec/api.md](../spec/api.md) + +--- + +## Message Integrity + +### RULE-06: Message order preservation +**Rule:** Messages within a single connection MUST be displayed in the order determined by the SMP agent's sequence numbers, not by local timestamps. +**Enforced by:** `Store/Messages.hs` (`createNewChatItem` uses agent-assigned ordering); `ItemsModel` in `ChatModel.swift` preserves this order. +**Spec:** [spec/state.md](../spec/state.md) + +### RULE-07: Edited messages retain history +**Rule:** When a message is edited, the previous version MUST be preserved in `chat_item_versions` and accessible via the item info view. +**Enforced by:** `Controller.hs` (`APIUpdateChatItem`); `Store/Messages.hs` (`updateChatItem` creates version record); `ChatItemInfoView.swift` displays history. +**Spec:** [spec/api.md](../spec/api.md) + +### RULE-08: Deleted messages respect deletion mode +**Rule:** `CIDeleteMode.cidmBroadcast` sends deletion to recipient; `cidmInternal` only deletes locally. Moderation deletion (`cidmInternalMark`) marks the item but retains a placeholder. +**Enforced by:** `Controller.hs` (`APIDeleteChatItem` checks `CIDeleteMode`); `MarkedDeletedItemView.swift` renders moderation placeholders. +**Spec:** [spec/api.md](../spec/api.md) + +### RULE-09: Timed messages auto-delete after TTL +**Rule:** Messages with a TTL MUST be automatically deleted from local storage after the configured time-to-live expires. +**Enforced by:** `Controller.hs` (background task scheduling); `Store/Messages.hs` (TTL-based cleanup). +**Spec:** [spec/api.md](../spec/api.md) + +--- + +## Group Integrity + +### RULE-10: Role hierarchy enforcement +**Rule:** A member can only modify members with strictly lower roles. Owner > Admin > Moderator > Member > Observer. +**Enforced by:** `Controller.hs` (`APIMembersRole` validates role hierarchy); `GroupMemberInfoView.swift` restricts available actions in UI. +**Spec:** [spec/api.md](../spec/api.md) + +### RULE-11: Group creator is always owner +**Rule:** The user who creates a group MUST be assigned the `GROwner` role and cannot be demoted. +**Enforced by:** `Controller.hs` (`APINewGroup`); `Store/Groups.hs` (`createNewGroup` assigns owner role). +**Spec:** [spec/api.md](../spec/api.md) + +### RULE-12: Group link role assignment +**Rule:** Members joining via group link MUST receive the role configured on the link (default: `GRMember`). Only admins and owners can create group links. +**Enforced by:** `Controller.hs` (`APICreateGroupLink` takes `memberRole` parameter); `GroupLinkView.swift` UI restricts to admin+. +**Spec:** [spec/api.md](../spec/api.md) + +--- + +## File Transfer + +### RULE-13: File size limits +**Rule:** Files up to 1GB are transferred via XFTP. The system MUST reject files exceeding the configured maximum. +**Enforced by:** Haskell core (`Files.hs` checks file size); XFTP protocol enforces chunk limits. +**Spec:** [spec/services/files.md](../spec/services/files.md) + +### RULE-14: File encryption at rest +**Rule:** When `privacyEncryptLocalFiles` is enabled, downloaded files MUST be encrypted locally using AES with per-file random key/nonce stored in `CryptoFile`. +**Enforced by:** `CryptoFile.swift` (`encryptCryptoFile`, `decryptCryptoFile`); `Library/Commands.hs` uses `CryptoFileArgs` for file encryption. +**Spec:** [spec/services/files.md](../spec/services/files.md) + +--- + +## Notification Delivery + +### RULE-15: Notification preview respects privacy setting +**Rule:** Notification content MUST respect `NotificationPreviewMode`: `.message` shows full content, `.contact` shows sender only, `.hidden` shows generic alert. +**Enforced by:** `Notifications.swift` (notification content creation checks `ntfPreviewModeGroupDefault`); `NotificationService.swift` (NSE content generation). +**Spec:** [spec/services/notifications.md](../spec/services/notifications.md) + +### RULE-16: NSE database coordination +**Rule:** The NSE and main app MUST NOT write to the database simultaneously. File locks coordinate access. +**Enforced by:** `chat_close_store` / `chat_reopen_store` FFI calls; NSE uses short-lived database sessions. +**Spec:** [spec/architecture.md](../spec/architecture.md) + +--- + +## Call Integrity + +### RULE-17: Call encryption key exchange +**Rule:** WebRTC call encryption keys MUST be negotiated over the existing E2E encrypted SMP channel, not through any external signaling server. +**Enforced by:** `ActiveCallView.swift` sends call signaling via `apiSendCallInvitation`/`apiSendCallAnswer` which use SMP; `Call.hs` defines call protocol. +**Spec:** [spec/services/calls.md](../spec/services/calls.md) + +### RULE-18: CallKit region restriction +**Rule:** CallKit MUST be disabled in regions where it is restricted (China). The app uses in-app call UI as fallback. +**Enforced by:** `CallController.swift` checks `useCallKit()` based on region; `ActiveCallView.swift` provides fallback UI. +**Spec:** [spec/services/calls.md](../spec/services/calls.md) diff --git a/apps/ios/product/views/call.md b/apps/ios/product/views/call.md new file mode 100644 index 0000000000..f32f7ec243 --- /dev/null +++ b/apps/ios/product/views/call.md @@ -0,0 +1,122 @@ +# Audio / Video Call + +> **Related spec:** [spec/services/calls.md](../../spec/services/calls.md) + +## Purpose + +Make and receive end-to-end encrypted audio and video calls over WebRTC. Supports CallKit integration for native iOS call UI, picture-in-picture for video calls, audio device selection, and collapsible call overlay. + +## Route / Navigation + +- **Entry point (outgoing)**: Tap audio or video call button in `ChatInfoView` action buttons or `ChatView` toolbar +- **Entry point (incoming)**: `IncomingCallView` banner appears at top of screen; or native CallKit UI if enabled +- **Presented by**: `ActiveCallView` is overlaid on the main app view when `chatModel.activeCall` is set +- **Collapsible**: Call view can be collapsed via `chatModel.activeCallViewIsCollapsed` to return to chat while call continues +- **Dismiss**: Call ends when user taps end button or remote party disconnects + +## Page Sections + +### Incoming Call Banner (`IncomingCallView`) + +Displayed as an overlay banner when `CallController.activeCallInvitation` is set: + +| Element | Description | +|---|---| +| Profile avatar | User profile image (shown when multiple profiles exist) | +| Call type icon | `video.fill` (green) for video calls, `phone.fill` (green) for audio | +| Call type text | "Audio call" or "Video call" with caller info | +| Caller profile | `ProfilePreview` showing caller name and image | +| Reject button | Red `phone.down.fill` icon -- ends the invitation | +| Ignore button | Neutral `multiply` icon -- dismisses the banner without rejecting | +| Accept button | Green `checkmark` icon -- accepts the call; if another call is active, ends it first | + +Sound: Ringtone plays via `SoundPlayer.startRingtone()` while banner is visible (unless call view is already showing). + +### Active Call View (`ActiveCallView`) + +Full-screen overlay with black background: + +| Element | Description | +|---|---| +| Remote video | Full-screen `CallViewRemote` showing remote party's camera feed; tap toggles between `scaleAspectFill` and `scaleAspectFit` | +| Local video preview | Small floating `CallViewLocal` in top-right corner (30% width); shows local camera with rounded corners | +| Call overlay | `ActiveCallOverlay` with call controls (hidden when PiP is active for video calls) | +| Screen keep-on | `AppDelegate.keepScreenOn(true)` prevents screen dimming during calls | + +### Call Controls (`ActiveCallOverlay`) + +Bottom bar of the active call: + +| Control | Description | +|---|---| +| Mute toggle | Microphone on/off | +| Speaker toggle | Speaker/receiver switch | +| Camera switch | Front/back camera toggle (video calls) | +| Video toggle | Enable/disable video during call | +| End call | Red phone-down button to terminate | +| Audio device picker | `AudioDevicePicker` / `CallAudioDeviceManager` for selecting output (receiver, speaker, Bluetooth, AirPods) | + +### Picture-in-Picture (PiP) + +- When `pipShown == true` and call has video, the call overlay is hidden +- PiP window shows the remote video feed +- User can interact with the app normally while call continues + +### CallKit Integration + +Managed by `CallController`: + +| Feature | Description | +|---|---| +| Native incoming call UI | iOS system call screen for incoming calls (when CallKit is enabled) | +| Call history | Optionally shown in Phone app recents (`DEFAULT_CALL_KIT_CALLS_IN_RECENTS`) | +| System audio routing | CallKit manages audio session configuration | +| Lock screen answering | Call can be answered from lock screen via system UI | + +When CallKit is not used, the app falls back to `IncomingCallView` banner. + +### WebRTC Client + +| Component | Description | +|---|---| +| `WebRTCClient` | Manages peer connection, ICE candidates, media tracks | +| `WebRTC.swift` | Bridge between native code and WebRTC JavaScript via `WKWebView` | +| `CallViewRenderers` | `CallViewLocal` and `CallViewRemote` SwiftUI wrappers for video renderers | + +## Loading / Error States + +| State | Behavior | +|---|---| +| Permissions required | Prompts for microphone (and camera for video) permissions on first call | +| Connecting | Call overlay shows connecting state; `SoundPlayer` plays connecting tone | +| WebRTC client creation | `createWebRTCClient()` called on appear and when `canConnectCall` changes | +| Call ended | `CallSoundsPlayer.vibrate(long: true)` on disconnect if was connected; audio session reset to `.soloAmbient` | +| Call failed | Call dismissed; WebRTC client cleaned up | +| No call invitation | `IncomingCallView` body is empty when no active invitation | + +## Audio Session Management + +- During call: Audio session configured for voice chat +- Camera permissions: `AVFoundation.AVCaptureDevice` authorization checked +- Audio device management: `CallAudioDeviceManager` handles routing changes and device enumeration +- Post-call cleanup: Audio session reverted to `.soloAmbient` + +## Related Specs + +- `spec/services/calls.md` -- Call service specification +- [Chat](chat.md) -- Call buttons in chat navigation bar +- [Contact Info](contact-info.md) -- Call buttons in contact info action row +- [Settings](settings.md) -- Call settings (CallKit, ICE servers, relay policy) + +## Source Files + +- `Shared/Views/Call/ActiveCallView.swift` -- Main active call view with video renderers and overlay +- `Shared/Views/Call/IncomingCallView.swift` -- Incoming call notification banner +- `Shared/Views/Call/CallController.swift` -- CallKit integration and call lifecycle management +- `Shared/Views/Call/CallManager.swift` -- Call state management and CXProvider delegate +- `Shared/Views/Call/CallAudioDeviceManager.swift` -- Audio device enumeration and routing +- `Shared/Views/Call/AudioDevicePicker.swift` -- Audio output device picker UI +- `Shared/Views/Call/WebRTC.swift` -- WebRTC signaling bridge via WKWebView +- `Shared/Views/Call/WebRTCClient.swift` -- WebRTC peer connection management +- `Shared/Views/Call/CallViewRenderers.swift` -- SwiftUI wrappers for local and remote video views +- `Shared/Views/Call/SoundPlayer.swift` -- Ringtone and call sound playback diff --git a/apps/ios/product/views/chat-list.md b/apps/ios/product/views/chat-list.md new file mode 100644 index 0000000000..6c2d868d64 --- /dev/null +++ b/apps/ios/product/views/chat-list.md @@ -0,0 +1,113 @@ +# Chat List (Home Screen) + +> **Related spec:** [spec/client/chat-list.md](../../spec/client/chat-list.md) + +## Purpose + +Main screen of the SimpleX Chat app. Displays all conversations sorted by last activity, serves as the navigation root, and provides access to user profiles, settings, and new chat creation. + +## Route / Navigation + +- **Entry point**: App launch (root view), or back-navigation from any chat +- **Presented by**: `ContentView` as the default view when `chatModel.chatId == nil` +- **Navigation stack**: `NavStackCompat` wrapping `chatListView` with destination `chatView` +- **UserPicker sheet**: Triggered by tapping the user avatar in the toolbar; presents `UserPicker` as a custom sheet, which links to `UserPickerSheetView` sub-sheets (address, preferences, profiles, current profile, use from desktop, settings) + +## Page Sections + +### Toolbar + +| Element | Location | Behavior | +|---|---|---| +| User avatar button | Leading | Opens `UserPicker` sheet (profile switcher, address, settings, preferences, connect to desktop) | +| Connection status indicator | Center (`SubsStatusIndicator`) | Shows server subscription status; taps navigate to `ServersSummaryView` | +| New chat button (pencil icon) | Trailing | Opens `NewChatSheet` modal | + +The toolbar supports two layout modes: +- **Standard (top)**: Navigation bar with `.topBarLeading`, `.principal`, `.topBarTrailing` placements +- **One-hand UI (bottom)**: Toolbar items placed in `.bottomBar` with the list vertically flipped via `scaleEffect(y: -1)` + +### Search Bar + +- Text field with magnifying glass icon +- When active, `searchMode = true` hides the navigation bar and shows inline search +- Filters chat list in real-time by contact/group name and message content +- Detects pasted SimpleX links (`searchShowingSimplexLink`) and offers to connect + +### Chat Filter Tabs (Tags) + +Managed by `ChatTagsModel` and `TagListView`: + +| Filter | PresetTag | Description | +|---|---|---| +| All | (none) | No filter, shows all chats | +| Unread | `.unread` | Chats with unread messages | +| Favorites | `.favorites` | User-favorited chats | +| Groups | `.groups` | Group conversations only | +| Contacts | `.contacts` | Direct contacts only | +| Business | `.business` | Business chat conversations | +| Notes | `.notes` | Notes to self | +| Group Reports | `.groupReports` | Moderation reports (non-collapsible) | +| Custom tags | `.userTag(ChatTag)` | User-created tags with custom names | + +### Chat Preview Rows + +Each row rendered by `ChatPreviewView` inside `ChatListNavLink`: + +| Element | Description | +|---|---| +| Avatar | Profile image or colored initials circle; online status indicator for contacts | +| Chat name | Display name (contact, group, or note-to-self) | +| Last message preview | Truncated text of most recent message; supports markdown rendering | +| Timestamp | Relative time of last activity (e.g., "2m", "1h", "Yesterday") | +| Unread badge | Numeric count badge for unread messages; distinct styling for mentions | +| Muted indicator | Bell-slash icon when notifications are muted | +| Pinned indicator | Pin icon for pinned chats | +| Incognito indicator | Shows when connected via incognito profile | +| Connection status | Shows connecting/pending state for incomplete connections | + +### Swipe Actions + +- **Trailing swipe**: Mute/unmute, pin/unpin, tag management +- **Leading swipe**: Mark as read/unread +- **Context menu** (long press): Full set of actions including delete, clear chat, toggle favorite + +### Floating Elements + +- **One-hand UI card** (`OneHandUICard`): Dismissible card shown to introduce bottom toolbar mode +- **Address creation card** (`AddressCreationCard`): Prompts user to create a SimpleX address + +### Pull-to-Refresh + +Triggers `reconnectAllServers()` after user confirmation alert ("Reconnect servers?"). Uses additional traffic to force message delivery. + +## Loading / Error States + +| State | Behavior | +|---|---| +| Chat database not started | Settings row shows exclamation icon; chat running == false disables interactions | +| No chats | `ChatHelp` view displayed with onboarding guidance | +| Connection in progress | `ConnectProgressManager` overlay with connecting text | +| Search with no results | Empty list with no special empty-state view | + +## Related Specs + +- `spec/client/chat-list.md` -- Chat list feature specification +- `spec/state.md` -- Application state management +- [User Profiles](user-profiles.md) -- Profile switching from UserPicker +- [Settings](settings.md) -- Settings accessed via UserPicker +- [New Chat](new-chat.md) -- New chat sheet triggered from toolbar +- [Chat](chat.md) -- Navigated to when tapping a chat row + +## Source Files + +- `Shared/Views/ChatList/ChatListView.swift` -- Main view, toolbar, search, filter logic +- `Shared/Views/ChatList/ChatPreviewView.swift` -- Individual chat row rendering +- `Shared/Views/ChatList/ChatListNavLink.swift` -- Navigation link wrapper with swipe actions +- `Shared/Views/ChatList/TagListView.swift` -- Filter tab bar (preset + custom tags) +- `Shared/Views/ChatList/UserPicker.swift` -- User profile picker sheet +- `Shared/Views/ChatList/ChatHelp.swift` -- Empty-state help view +- `Shared/Views/ChatList/ContactRequestView.swift` -- Contact request row rendering +- `Shared/Views/ChatList/ContactConnectionView.swift` -- Pending connection row rendering +- `Shared/Views/ChatList/OneHandUICard.swift` -- One-hand UI introduction card +- `Shared/Views/ChatList/ServersSummaryView.swift` -- Server subscription summary diff --git a/apps/ios/product/views/chat.md b/apps/ios/product/views/chat.md new file mode 100644 index 0000000000..57202846eb --- /dev/null +++ b/apps/ios/product/views/chat.md @@ -0,0 +1,165 @@ +# Chat View (Conversation) + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) | [spec/client/compose.md](../../spec/client/compose.md) + +## Purpose + +Full conversation view for displaying and interacting with messages in a direct contact chat, group chat, or note-to-self. Supports text messaging with markdown, media attachments, voice messages, E2E encrypted calls, message reactions, replies, forwarding, and content search/filtering. + +## Route / Navigation + +- **Entry point**: Tap a chat row in `ChatListView` +- **Presented by**: `NavStackCompat` destination from `ChatListView`, bound to `chatModel.chatId` +- **Back navigation**: Dismiss sets `chatModel.chatId = nil`, returning to chat list +- **Sub-navigation**: Info button navigates to `ChatInfoView` (contact) or `GroupChatInfoView` (group); member avatars navigate to `GroupMemberInfoView` + +## Page Sections + +### Navigation Bar + +Custom toolbar overlaying the chat with themed material background: + +| Element | Description | +|---|---| +| Back button | Returns to chat list | +| Contact/Group avatar | Small profile image | +| Chat name | Display name; tappable to open info sheet | +| Encryption badge | Shows PQ (post-quantum) or standard E2E status | +| Call buttons | Audio and video call icons (direct chats only) | +| Search button | Toggles in-chat message search | +| Info button | Opens `ChatInfoView` or `GroupChatInfoView` | + +### Message List + +Rendered by `EndlessScrollView` with lazy loading and pagination: + +| Feature | Description | +|---|---| +| Scroll direction | Bottom-to-top (newest messages at bottom) | +| Pagination | Loads more items on scroll to top (`loadingTopItems`) and bottom (`loadingBottomItems`) | +| Merged items | Adjacent messages from the same sender are visually merged via `MergedItems` | +| Floating buttons | Scroll-to-bottom button with unread count; scroll-to-first-unread button | +| Date separators | Sticky date headers between messages from different days | +| Wallpaper | Themed background image with tint and opacity from `theme.wallpaper` | +| Content filter | Filter messages by type: `.images`, `.files`, `.links` | + +### Message Types + +Each type has a dedicated view in `Shared/Views/Chat/ChatItem/`: + +| Type | View | Description | +|---|---|---| +| Text | `MsgContentView` | Rendered with markdown (bold, italic, code, links, mentions) | +| Image | `CIImageView` | Thumbnail with tap-to-fullscreen via `FullScreenMediaView` | +| Video | `CIVideoView` | Video thumbnail with play button; inline playback | +| Voice | `CIVoiceView` / `FramedCIVoiceView` | Waveform visualization with playback controls and duration | +| File | `CIFileView` | File icon, name, size; download/open actions | +| Link preview | `CILinkView` | URL preview card with title, description, image | +| Emoji-only | `EmojiItemView` | Large emoji rendering without message bubble | +| Call event | `CICallItemView` | Call status (missed, ended, duration) | +| Group event | `CIEventView` | Member joined/left, role changes, group updates | +| E2EE info | `CIChatFeatureView` | Encryption status and feature change notifications | +| Group invitation | `CIGroupInvitationView` | Inline group join invitation card | +| Deleted | `DeletedItemView` / `MarkedDeletedItemView` | Placeholder for deleted messages | +| Decryption error | `CIRcvDecryptionError` | Error with ratchet sync suggestion | +| Invalid JSON | `CIInvalidJSONView` | Developer fallback for malformed items | +| Integrity error | `IntegrityErrorItemView` | Message integrity/gap warnings | + +### Message Interactions + +Long-press context menu on any message: + +| Action | Description | +|---|---| +| Reply | Sets compose bar to reply mode with quoted message | +| Forward | Opens `forwardedChatItems` sheet to pick destination chat | +| Copy | Copies message text to clipboard | +| Edit | Enters edit mode in compose bar (own messages, within edit window) | +| Delete | Delete for self or delete for everyone (with confirmation) | +| React | Opens emoji reaction picker | +| Select multiple | Enters multi-select mode (`selectedChatItems`) with bulk delete/forward | +| Info | Shows delivery status and timestamps | + +Emoji reactions bar displayed below messages with reaction counts. + +### Compose Bar (`ComposeView`) + +| Element | Description | +|---|---| +| Text input | `NativeTextEditor` with markdown support and auto-growing height | +| Attachment button | Opens picker for images, videos, files, camera | +| Send button | Sends composed message; changes to voice record button when empty | +| Voice record | Hold-to-record with waveform preview; swipe-to-cancel | +| Reply quote | Shows quoted message above input when replying | +| Edit indicator | Shows "editing" label when editing a previous message | +| Link preview | Auto-generated preview card for detected URLs (`ComposeLinkView`) | +| Image/Video preview | Thumbnail strip for selected media (`ComposeImageView`) | +| File preview | File name and size for attached file (`ComposeFileView`) | +| Voice preview | Waveform of recorded voice message (`ComposeVoiceView`) | +| Live message | Real-time typing broadcast (optional, with alert on first use) | +| Context actions | `ContextContactRequestActionsView` for accepting/rejecting contact requests; `ContextPendingMemberActionsView` for pending group member actions | +| Commands menu | `CommandsMenuView` for bot/menu commands in chats with `menuCommands` | +| Group mentions | `GroupMentionsView` autocomplete popup when typing `@` in groups | +| Profile picker | `ContextProfilePickerView` for choosing incognito/main profile | + +### Member Support Chat (Groups) + +For groups with member support enabled: +- `MemberSupportView` and `MemberSupportChatToolbar` shown as secondary chat within group +- `SecondaryChatView` for scoped group chat views (reports, member support) +- User knocking state: `userMemberKnockingTitleBar()` shown when user is pending admission + +## Loading / Error States + +| State | Behavior | +|---|---| +| Initial load | Messages load from `ItemsModel` with merged items; `allowLoadMoreItems` throttles pagination | +| Loading more (top) | `loadingTopItems` spinner at top of scroll view | +| Loading more (bottom) | `loadingBottomItems` spinner at bottom | +| Connection in progress | `ConnectProgressManager` shows connecting text below compose bar | +| Connecting text | "connecting..." label shown below message list when chat not yet ready | +| Send disabled | Compose bar shows `disabledText` reason when `userCantSendReason` is set | +| Empty chat | No messages placeholder (implicit -- empty scroll view) | + +## Related Specs + +- `spec/client/chat-view.md` -- Chat view feature specification +- `spec/client/compose.md` -- Compose bar specification +- [Chat List](chat-list.md) -- Parent navigation +- [Contact Info](contact-info.md) -- Info sheet for direct chats +- [Group Info](group-info.md) -- Info sheet for group chats +- [Call](call.md) -- Audio/video calls initiated from toolbar + +## Source Files + +- `Shared/Views/Chat/ChatView.swift` -- Main chat view, message list, navigation, state management +- `Shared/Views/Chat/ChatItemView.swift` -- Individual message item rendering dispatcher +- `Shared/Views/Chat/ComposeMessage/ComposeView.swift` -- Compose bar container +- `Shared/Views/Chat/ComposeMessage/SendMessageView.swift` -- Send button and voice record +- `Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift` -- Text input with markdown +- `Shared/Views/Chat/ComposeMessage/ComposeImageView.swift` -- Image attachment preview +- `Shared/Views/Chat/ComposeMessage/ComposeFileView.swift` -- File attachment preview +- `Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift` -- Voice recording preview +- `Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift` -- Link preview generation +- `Shared/Views/Chat/ComposeMessage/ContextItemView.swift` -- Reply/edit context display +- `Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift` -- Contact request accept/reject +- `Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift` -- Pending member actions +- `Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift` -- Profile picker for incognito +- `Shared/Views/Chat/ChatItem/FramedItemView.swift` -- Framed message bubble rendering +- `Shared/Views/Chat/ChatItem/MsgContentView.swift` -- Text message content with markdown +- `Shared/Views/Chat/ChatItem/CIImageView.swift` -- Image message view +- `Shared/Views/Chat/ChatItem/CIVideoView.swift` -- Video message view +- `Shared/Views/Chat/ChatItem/CIVoiceView.swift` -- Voice message view +- `Shared/Views/Chat/ChatItem/CIFileView.swift` -- File message view +- `Shared/Views/Chat/ChatItem/CILinkView.swift` -- Link preview view +- `Shared/Views/Chat/ChatItem/EmojiItemView.swift` -- Large emoji view +- `Shared/Views/Chat/ChatItem/CICallItemView.swift` -- Call event view +- `Shared/Views/Chat/ChatItem/CIEventView.swift` -- Group/system event view +- `Shared/Views/Chat/ChatItem/CIChatFeatureView.swift` -- Feature change notification +- `Shared/Views/Chat/ChatItem/CIMetaView.swift` -- Timestamp and delivery status +- `Shared/Views/Chat/ChatItem/FullScreenMediaView.swift` -- Fullscreen image/video viewer +- `Shared/Views/Chat/ChatItem/AnimatedImageView.swift` -- Animated GIF rendering +- `Shared/Views/Chat/Group/GroupMentions.swift` -- @mention autocomplete +- `Shared/Views/Chat/Group/MemberSupportView.swift` -- Member support scoped chat +- `Shared/Views/Chat/Group/MemberSupportChatToolbar.swift` -- Support chat toolbar +- `Shared/Views/Chat/Group/SecondaryChatView.swift` -- Secondary scoped chat view diff --git a/apps/ios/product/views/contact-info.md b/apps/ios/product/views/contact-info.md new file mode 100644 index 0000000000..5223bfcae4 --- /dev/null +++ b/apps/ios/product/views/contact-info.md @@ -0,0 +1,154 @@ +# Contact Info + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Purpose + +View contact details, manage per-contact preferences, verify security codes for E2E encryption, manage connection settings, and perform destructive actions like blocking or deleting a contact. + +## Route / Navigation + +- **Entry point**: Tap the info button in `ChatView` navigation bar (when viewing a direct contact chat) +- **Presented by**: `NavigationView` sheet from `ChatView` via `showChatInfoSheet` +- **Sub-navigation**: + - Contact preferences -> `ContactPreferencesView` + - Security code verification -> `VerifyCodeView` + - Chat wallpaper -> `ChatWallpaperEditorSheet` + +## Page Sections + +### Contact Info Header + +| Element | Description | +|---|---| +| Profile image | Large circular avatar; tappable | +| Display name | Contact's display name | +| Full name | Optional full name below display name | +| Connection status | Shows if contact is ready, connecting, or has issues | + +### Local Alias + +Editable text field (`aliasTextFieldFocused`) for setting a local-only name visible only on this device. Not shared with the contact. + +### Action Buttons + +Horizontal row of quick-action buttons (width divided by 4): + +| Button | Description | +|---|---| +| Search | Triggers `onSearch` to search messages in chat | +| Audio call | Initiate audio call (`AudioCallButton`) | +| Video call | Initiate video call (`VideoButton`) | +| Mute/Unmute | Toggle notification mode (`nextNtfMode`) | + +Call buttons check `connectionStats` and show alerts if connection state prevents calling. + +### Incognito Section + +Shown only when `customUserProfile` is set (connected via incognito): + +| Element | Description | +|---|---| +| "Your random profile" label | Shows the incognito display name used for this contact | + +### Connection Settings Section + +| Element | Condition | Description | +|---|---|---| +| Verify security code | `connectionCode` available | Navigate to `VerifyCodeView` for QR-based code verification | +| Contact preferences | Always | Navigate to `ContactPreferencesView` | +| Send receipts | Always | Toggle: yes / no / default(yes) / default(no) | +| Synchronize connection | `ratchetSyncAllowed` | Fix encryption ratchet desynchronization | +| Chat theme | Always | Navigate to `ChatWallpaperEditorSheet` | + +All items disabled when `!contact.ready || !contact.active`. + +### Chat TTL Section + +| Element | Description | +|---|---| +| Chat TTL option | `ChatTTLOption` -- auto-delete timer for messages on this device | + +Footer: "Delete chat messages from your device." + +### Encryption Info Section + +Shown when `contact.activeConn` exists: + +| Element | Description | +|---|---| +| E2E encryption | "Quantum resistant" (PQ enabled) or "Standard" | + +### Contact Address Section + +Shown when `contact.contactLink` exists: + +| Element | Description | +|---|---| +| QR code | `SimpleXLinkQRCode` displaying the contact's address | +| Share address | Share button for the contact's SimpleX address link | + +Footer: "You can share this address with your contacts to let them connect with **[name]**." + +### Servers Section + +Shown when `contact.ready && contact.active`: + +| Element | Description | +|---|---| +| Subscription status | `SubStatusRow` showing connection health; tappable for details | +| Change receiving address | Button to switch SMP receiving queue (disabled during switch) | +| Abort changing address | Button to cancel in-progress address switch | +| Receiving via | SMP server hostnames for receiving queues | +| Sending via | SMP server hostnames for sending queues | + +### Danger Zone Section + +| Action | Description | +|---|---| +| Clear chat | Delete all messages locally (confirmation alert) | +| Delete contact | Remove contact entirely (confirmation alert) | + +### Developer Section + +Shown when `developerTools` is enabled: + +| Element | Description | +|---|---| +| Local name | Internal local display name | +| Database ID | API entity ID | +| Debug delivery | Button to fetch queue info via `apiContactQueueInfo` | + +## Loading / Error States + +| State | Behavior | +|---|---| +| Loading connection info | `apiContactInfo` and `apiGetContactCode` called on appear; stats and code populated asynchronously | +| Progress indicator | `ProgressView` overlay during TTL changes | +| Contact not ready | Settings section disabled with reduced opacity | +| Contact inactive | Settings section disabled | +| Errors | Alert with localized error title and message | + +## Alerts + +| Alert | Trigger | +|---|---| +| `clearChatAlert` | Tap clear chat | +| `subStatusAlert` | Tap subscription status row | +| `switchAddressAlert` | Tap change receiving address | +| `abortSwitchAddressAlert` | Tap abort address change | +| `syncConnectionForceAlert` | Force ratchet sync | +| `queueInfo` | Debug delivery results | +| `someAlert` | Various sub-component alerts | + +## Related Specs + +- `spec/api.md` -- Contact API commands (info, code verification, preferences, delete) +- [Chat](chat.md) -- Parent chat view +- [Group Info](group-info.md) -- Similar pattern for group info + +## Source Files + +- `Shared/Views/Chat/ChatInfoView.swift` -- Main contact info view with all sections +- `Shared/Views/Chat/ContactPreferencesView.swift` -- Per-contact feature preferences (timed messages, reactions, voice, calls, file transfer, full delete) +- `Shared/Views/Chat/VerifyCodeView.swift` -- Security code verification via QR scan or visual comparison diff --git a/apps/ios/product/views/group-info.md b/apps/ios/product/views/group-info.md new file mode 100644 index 0000000000..9291b3ed2f --- /dev/null +++ b/apps/ios/product/views/group-info.md @@ -0,0 +1,147 @@ +# Group Chat Info + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Purpose + +View and manage group settings, member list, group preferences, group links, member admission, welcome messages, and moderation features. The scope of available actions depends on the user's role within the group (member, moderator, admin, owner). + +## Route / Navigation + +- **Entry point**: Tap the info button in `ChatView` navigation bar (when viewing a group chat) +- **Presented by**: `NavigationView` sheet from `ChatView` via `showChatInfoSheet` +- **Sub-navigation**: + - Edit group profile -> `GroupProfileView` + - Add members -> `AddGroupMembersView` + - Group link -> `GroupLinkView` + - Group preferences -> `GroupPreferencesView` (via `GroupPreferencesButton`) + - Welcome message -> `GroupWelcomeView` + - Member info -> `GroupMemberInfoView` + - Chat wallpaper -> `ChatWallpaperEditorSheet` + - Member support -> `MemberSupportView` + - Group reports -> `GroupReportsChatNavLink` + +## Page Sections + +### Group Info Header + +| Element | Description | +|---|---| +| Group image | Large circular profile image | +| Group name | Display name (editable by owners) | +| Member count | "N members" label | +| Full name | Optional secondary name | +| Description | Group description text (if set) | + +### Local Alias + +Editable text field for a local-only alias (not shared with other members). Focused via `aliasTextFieldFocused`. + +### Action Buttons + +Horizontal row of action buttons: + +| Button | Description | +|---|---| +| Search | Triggers `onSearch` callback to search messages in chat | +| Mute/Unmute | Toggle notification mode (`nextNtfMode`) | + +### Group Management Section + +| Element | Condition | Description | +|---|---|---| +| Group link | `canAddMembers` and not business chat | Navigate to `GroupLinkView` to create/manage invitation link | +| Member support | Not business chat, role >= moderator | Navigate to member support chat view | +| Group reports | `canModerate` | Navigate to group reports chat | +| User support chat | Member active, role < moderator or has support chat | Navigate to own support chat with moderators | + +### Group Profile Section + +| Element | Condition | Description | +|---|---|---| +| Edit group | Owner, not business chat | Navigate to `GroupProfileView` for editing name, image, description | +| Welcome message | Has description or is owner (not business) | Navigate to `GroupWelcomeView` for add/edit | +| Group preferences | Always | Navigate to `GroupPreferencesView` -- timed messages, reactions, voice, files, direct messages, history visibility | + +Footer: "Only group owners can change group preferences." (or "Only chat owners can change preferences." for business chats) + +### Chat Settings Section + +| Element | Description | +|---|---| +| Send receipts | Toggle delivery receipts; disabled for groups > 20 current members with explanation | +| Chat theme | Navigate to `ChatWallpaperEditorSheet` | +| Chat TTL | `ChatTTLOption` -- set auto-deletion timer for messages on device | + +Footer: "Delete chat messages from your device." + +### Member List Section + +Header shows total member count (e.g., "25 members"). + +| Element | Description | +|---|---| +| Invite members button | Shown if `canAddMembers`; disabled with tap alert if incognito | +| Search field | Filter members by name (`searchText`) | +| Member rows | Each shows: avatar, display name, role badge (owner/admin/moderator/observer), online status indicator, connection status | +| Member tap | Navigates to `GroupMemberInfoView` | +| Member swipe actions | Block/unblock member, block/unblock for all (moderators) | + +Member list is sorted by role (owners first) and filtered to exclude `memLeft` and `memRemoved` statuses. + +### Danger Zone Section + +| Action | Description | +|---|---| +| Clear chat | Deletes all messages locally (with confirmation alert) | +| Leave group | Leave the group (with confirmation alert) | +| Delete group | Delete entire group -- only for owners (with confirmation alert) | + +### Developer Section + +Shown when `developerTools` is enabled: + +| Element | Description | +|---|---| +| Local name | Internal chat local display name | +| Database ID | API entity ID | + +## Loading / Error States + +| State | Behavior | +|---|---| +| Loading members | Member list populated from `chatModel.groupMembers` | +| Progress indicator | `ProgressView` overlay when `progressIndicator` is true (during TTL changes) | +| Large group receipts | Receipts option disabled with "Disabled for large groups" label and info alert | +| Incognito invite blocked | Alert: "Can't invite contacts when incognito" | +| Errors | Alert with localized title and error description | + +## Alerts + +| Alert | Trigger | +|---|---| +| `deleteGroupAlert` | Tap delete group | +| `clearChatAlert` | Tap clear chat | +| `leaveGroupAlert` | Tap leave group | +| `cantInviteIncognitoAlert` | Tap invite members while incognito | +| `largeGroupReceiptsDisabled` | Tap receipts info on large group | +| `blockMemberAlert` / `unblockMemberAlert` | Block/unblock member actions | +| `blockForAllAlert` / `unblockForAllAlert` | Moderator block/unblock for all members | + +## Related Specs + +- `spec/api.md` -- Group API commands (create, update, add/remove members, roles, links) +- [Chat](chat.md) -- Parent chat view +- [Contact Info](contact-info.md) -- Similar pattern for direct contact info + +## Source Files + +- `Shared/Views/Chat/Group/GroupChatInfoView.swift` -- Main group info view with all sections +- `Shared/Views/Chat/Group/GroupProfileView.swift` -- Edit group name, image, description +- `Shared/Views/Chat/Group/AddGroupMembersView.swift` -- Member invitation view +- `Shared/Views/Chat/Group/GroupLinkView.swift` -- Group link creation and management +- `Shared/Views/Chat/Group/GroupPreferencesView.swift` -- Group feature preferences +- `Shared/Views/Chat/Group/GroupWelcomeView.swift` -- Welcome message editor +- `Shared/Views/Chat/Group/MemberAdmissionView.swift` -- Member admission policy settings +- `Shared/Views/Chat/Group/GroupMemberInfoView.swift` -- Individual member info and actions +- `Shared/Views/Chat/Group/GroupMentions.swift` -- @mention support in groups diff --git a/apps/ios/product/views/new-chat.md b/apps/ios/product/views/new-chat.md new file mode 100644 index 0000000000..e53659e622 --- /dev/null +++ b/apps/ios/product/views/new-chat.md @@ -0,0 +1,94 @@ +# New Chat / Connection + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) + +## Purpose + +Create new contacts, groups, or connect with others via one-time invitation links or by scanning/pasting SimpleX links. This is the primary onramp for establishing new E2E encrypted connections. + +## Route / Navigation + +- **Entry point**: Tap the new chat button (pencil icon) in `ChatListView` toolbar +- **Presented by**: `NewChatSheet` modal from `ChatListView` +- **Internal navigation**: `NewChatMenuButton` provides a dropdown with options: + - "New chat" -- opens `NewChatView` + - "Create group" -- opens `AddGroupView` +- **Tabs within NewChatView**: Segmented picker toggles between `.invite` (1-time link) and `.connect` (connect via link) +- **Swipe gesture**: Left/right swipe switches between invite and connect tabs +- **Dismiss behavior**: On dismiss, `showKeepInvitationAlert()` asks whether to keep an unused invitation link or delete it + +## Page Sections + +### Segmented Picker + +| Tab | Icon | Description | +|---|---|---| +| 1-time link | `link` | Generate and share a one-time invitation link | +| Connect via link | `qrcode` | Scan QR code or paste a received link | + +### Invite Tab (1-time Link) + +Displayed when `selection == .invite`: + +| Element | Description | +|---|---| +| QR code display | Generated QR code for the invitation link (`SimpleXLinkQRCode`) | +| Short/full link toggle | Switch between short and full link display | +| Share button | System share sheet for the invitation link | +| Copy button | Copy link to clipboard | +| Incognito toggle | Option to connect with a random profile | +| Loading state | `creatingLinkProgressView` spinner while `creatingConnReq` is true | +| Retry button | Shown if link creation fails | + +Link creation calls `apiAddContact` which returns a `CreatedConnLink` with both `connFullLink` and optional `connShortLink`. + +### Connect Tab (Connect via Link) + +Displayed when `selection == .connect`: + +| Element | Description | +|---|---| +| QR code scanner | Camera-based `CodeScanner` view for scanning SimpleX QR codes | +| Paste link field | Text input for pasting a SimpleX link manually | +| Connect button | Initiates connection via the pasted/scanned link | + +Handled by `ConnectView` sub-view with `showQRCodeScanner` state. + +### Info Sheet + +Toolbar trailing button opens `AddContactLearnMore` info sheet explaining how SimpleX connections work. + +### Add Group + +Accessed via `NewChatMenuButton` dropdown: + +| Element | Description | +|---|---| +| Group name | Required text field | +| Group image | Optional profile image picker | +| Incognito option | Create group with random profile | +| Create button | Creates group via API and navigates to group chat | + +## Loading / Error States + +| State | Behavior | +|---|---| +| Creating invitation | `ProgressView` spinner shown; buttons disabled | +| Link creation failure | Retry button displayed | +| Invalid link pasted | Alert shown via `NewChatViewAlert.newChatSomeAlert` | +| Connection in progress | Chat list shows pending connection entry | +| Unused invitation on dismiss | Alert: "Keep unused invitation?" with Keep/Delete options | + +## Related Specs + +- `spec/api.md` -- API commands: `APIAddContact`, `APIConnect`, `APICreateUserAddress` +- [Chat List](chat-list.md) -- Parent view that presents this sheet +- [Chat](chat.md) -- Navigated to after successful connection + +## Source Files + +- `Shared/Views/NewChat/NewChatView.swift` -- Main view with invite/connect tabs, link generation +- `Shared/Views/NewChat/NewChatMenuButton.swift` -- Dropdown menu (new chat, create group) +- `Shared/Views/NewChat/QRCode.swift` -- QR code generation and display +- `Shared/Views/NewChat/AddGroupView.swift` -- Group creation form +- `Shared/Views/NewChat/AddContactLearnMore.swift` -- Info sheet explaining connection process diff --git a/apps/ios/product/views/onboarding.md b/apps/ios/product/views/onboarding.md new file mode 100644 index 0000000000..a283c25a19 --- /dev/null +++ b/apps/ios/product/views/onboarding.md @@ -0,0 +1,147 @@ +# Onboarding + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/architecture.md](../../spec/architecture.md) + +## Purpose + +First-time setup flow for new users. Guides through app introduction, profile creation, server operator conditions acceptance, and notification configuration. Also provides an entry point for device migration. + +## Route / Navigation + +- **Entry point**: App launch when `onboardingStageDefault` is not `.onboardingComplete` +- **Presented by**: `OnboardingView` renders the appropriate step based on `OnboardingStage` enum +- **Flow direction**: Linear progression; back navigation hidden on later steps (`.navigationBarBackButtonHidden(true)`) +- **Completion**: Sets `onboardingStageDefault` to `.onboardingComplete` and updates `chatModel.onboardingStage` + +## Onboarding Steps + +### Step 1: Welcome / SimpleX Info (`SimpleXInfo`) + +**Stage**: `step1_SimpleXInfo` + +| Element | Description | +|---|---| +| Logo | SimpleX Chat logo (light/dark variant based on color scheme) | +| "The future of messaging" | Info button opening `HowItWorks` sheet | +| Privacy redefined | "No user identifiers." with privacy icon | +| Immune to spam | "You decide who can connect." with shield icon | +| Decentralized | "Anybody can host servers." with decentralized icon | +| **Create your profile** button | Primary action; navigates to `CreateFirstProfile` | +| **Migrate from another device** button | Secondary action; opens `MigrateToDevice` sheet | + +The "How it works" sheet (`HowItWorks`) explains SimpleX's privacy model with an option to proceed to profile creation. + +### Step 2: Create Profile (`CreateFirstProfile`) + +**Stage**: `step2_CreateProfile` (deprecated -- now part of step 1 flow) + +| Element | Description | +|---|---| +| Display name field | Required; auto-focused after 1 second delay | +| Validation | `mkValidName` check; alerts for invalid/duplicate names | +| Create button | Calls profile creation API; advances to next step | + +Profile is stored locally and only shared with contacts. Footer explains this privacy property. + +### Step 3: Server Operator Conditions (`OnboardingConditionsView`) + +**Stage**: `step3_ChooseServerOperators` (changed to simplified conditions view) + +| Element | Description | +|---|---| +| "Conditions of use" title | Large title header | +| Privacy explanation | "Private chats, groups and your contacts are not accessible to server operators." | +| Operator selection | Toggle operators (with `selectedOperatorIds`) | +| Show conditions | Sheet to view full conditions (`ConditionsWebView`) | +| Configure operators | Sheet to customize operator settings | +| **Accept** button | Accepts conditions and advances to notifications step | + +Previous deprecated step `step3_CreateSimpleXAddress` (`CreateSimpleXAddress`) is no longer in the active flow. + +### Step 4: Set Notification Mode (`SetNotificationsMode`) + +**Stage**: `step4_SetNotificationsMode` + +| Element | Description | +|---|---| +| "Push notifications" title | Large title header | +| Info text | Explanation of notification modes | +| Mode selector | `NtfModeSelector` for each `NotificationsMode.values` | +| **Enable notifications** / **Use chat** button | Sets notification mode and completes onboarding | +| Info sheet | `NotificationsInfoView` accessible for detailed explanation | + +Notification modes: + +| Mode | Description | +|---|---| +| Instant | Background connection maintained; real-time notifications | +| Periodic | Checks every 10 minutes; battery-friendly | +| Off | No push notifications; messages received only when app is open | + +On completion, `onboardingStageDefault.set(.onboardingComplete)` is called. + +### Completion + +**Stage**: `onboardingComplete` + +`OnboardingView` renders `EmptyView()` and the app proceeds to `ChatListView`. + +## Optional Paths + +### Migrate from Another Device + +- Triggered from Step 1 via "Migrate from another device" button +- Sets `chatModel.migrationState = .pasteOrScanLink` +- Opens `MigrateToDevice` in a sheet within `NavigationView` +- User pastes or scans a migration link from the source device +- Imports database and settings from the linked device + +### What's New (`WhatsNewView`) + +- Not part of the linear onboarding flow +- Shown when `DEFAULT_WHATS_NEW_VERSION` differs from current version +- Accessible later from Settings > Help > What's new +- Displays changelog with feature descriptions + +## Onboarding Stage Enum + +``` +enum OnboardingStage: String { + case step1_SimpleXInfo + case step2_CreateProfile // deprecated + case step3_CreateSimpleXAddress // deprecated + case step3_ChooseServerOperators // conditions acceptance + case step4_SetNotificationsMode + case onboardingComplete +} +``` + +Persisted via `DEFAULT_ONBOARDING_STAGE` in `UserDefaults`. + +## Loading / Error States + +| State | Behavior | +|---|---| +| No device token | Alert "No device token!" if trying to set notification mode without token | +| Profile creation error | Alert with error description | +| Migration failure | Error handling within `MigrateToDevice` flow | +| Conditions loading | Async fetch of operator conditions | + +## Related Specs + +- `spec/architecture.md` -- App architecture and initialization flow +- [Chat List](chat-list.md) -- Destination after onboarding completes +- [User Profiles](user-profiles.md) -- Profile created during onboarding; additional profiles later +- [Settings](settings.md) -- Notification and server settings revisitable after onboarding + +## Source Files + +- `Shared/Views/Onboarding/OnboardingView.swift` -- Step router and `OnboardingStage` enum definition +- `Shared/Views/Onboarding/SimpleXInfo.swift` -- Step 1: Welcome screen with privacy highlights and migration entry +- `Shared/Views/Onboarding/CreateProfile.swift` -- Profile creation form (shared between onboarding and user profiles) +- `Shared/Views/Onboarding/CreateSimpleXAddress.swift` -- Deprecated step 3: SimpleX address creation +- `Shared/Views/Onboarding/ChooseServerOperators.swift` -- Step 3: Server operator conditions and selection +- `Shared/Views/Onboarding/SetNotificationsMode.swift` -- Step 4: Push notification mode selection +- `Shared/Views/Onboarding/HowItWorks.swift` -- "How it works" info sheet from step 1 +- `Shared/Views/Onboarding/WhatsNewView.swift` -- Changelog / what's new display +- `Shared/Views/Onboarding/AddressCreationCard.swift` -- Address creation prompt card diff --git a/apps/ios/product/views/settings.md b/apps/ios/product/views/settings.md new file mode 100644 index 0000000000..58507ce52b --- /dev/null +++ b/apps/ios/product/views/settings.md @@ -0,0 +1,172 @@ +# Settings + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/services/theme.md](../../spec/services/theme.md) | [spec/services/notifications.md](../../spec/services/notifications.md) + +## Purpose + +Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and developer tools. Accessed from the UserPicker sheet on the chat list. + +## Route / Navigation + +- **Entry point**: Tap user avatar in `ChatListView` toolbar -> `UserPicker` -> Settings option +- **Presented by**: `UserPickerSheetView(sheet: .settings)` wrapping `SettingsView` in a `NavigationView` +- **Navigation title**: "Your settings" +- **Sub-navigation**: Each settings row is a `NavigationLink` to a dedicated settings view + +## Page Sections + +### Settings Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| Notifications | `bolt` (color varies by token status) | `NotificationsView` | Push notification mode and preview settings | +| Network & servers | `externaldrive.connected.to.line.below` | `NetworkAndServers` | SMP/XFTP servers, proxy, .onion hosts, advanced network | +| Audio & video calls | `video` | `CallSettings` | WebRTC relay policy, ICE servers, CallKit options | +| Privacy & security | `lock` | `PrivacySettings` | SimpleX Lock, screen protection, delivery receipts, auto-accept | +| Appearance | `sun.max` | `AppearanceSettings` | Theme, language, wallpapers, chat bubbles, toolbar opacity | + +All rows disabled when `chatModel.chatRunning != true`. Appearance row only shown when `UIApplication.shared.supportsAlternateIcons`. + +#### Notifications (`NotificationsView`) + +| Setting | Options | +|---|---| +| Notification mode | Instant (background connection) / Periodic (every 10 min) / Off | +| Notification preview | Hidden / Contact name only / Message preview | +| Token status indicator | Icon color reflects: new, registered, confirmed (yellow), active (green), expired, invalid | + +#### Network & Servers (`NetworkAndServers`) + +| Setting | Description | +|---|---| +| SMP servers | Messaging relay servers; per-operator configuration | +| XFTP servers | File transfer servers; per-operator configuration | +| Server operators | `OperatorView` for each configured operator | +| Advanced network | `AdvancedNetworkSettings` -- timeouts, TCP keep-alive, reconnect intervals | +| Proxy configuration | SOCKS proxy, .onion host settings | +| Show sent via proxy | Toggle to show proxy indicator on sent messages | +| Show subscription % | Toggle to show server subscription percentage | + +Sub-files: `NetworkAndServers.swift`, `ProtocolServersView.swift`, `ProtocolServerView.swift`, `NewServerView.swift`, `ScanProtocolServer.swift`, `AdvancedNetworkSettings.swift`, `OperatorView.swift`, `ConditionsWebView.swift` + +#### Privacy & Security (`PrivacySettings`) + +| Setting | Description | +|---|---| +| SimpleX Lock | Enable biometric (Face ID / Touch ID) or passcode lock | +| Lock mode | System biometric or custom passcode | +| Lock timeout | Delay before lock activates (0s to 30min) | +| Self-destruct | Optional self-destruct passcode that wipes all data | +| Screen protection | Hide app content in app switcher | +| Encrypt local files | Encrypt media and files stored on device | +| Auto-accept images | Automatically download received images | +| Link previews | Generate link previews for sent URLs | +| SimpleX link mode | Description / Full link / Via browser | +| Chat previews | Show message previews in chat list | +| Save last draft | Remember unsent message drafts | +| Delivery receipts | Enable/disable read receipts globally | +| Media blur radius | Blur level for received media before tapping | + +#### Appearance (`AppearanceSettings`) + +| Setting | Description | +|---|---| +| App icon | Alternative app icon selection | +| Language | Interface language | +| Theme | System / Light / Dark | +| Dark theme variant | Dark / SimpleX / Black | +| Active theme colors | Accent color, chat bubble colors, text colors | +| Wallpapers | Chat background wallpaper selection and customization | +| Profile image corner radius | Adjust avatar roundness | +| Chat bubble roundness | Adjust message bubble corner radius | +| Chat bubble tail | Toggle message bubble tail/pointer | +| Toolbar opacity | `ToolbarMaterial` transparency setting | +| One-hand UI | Bottom toolbar layout for reachability | + +#### Audio & Video Calls (`CallSettings`) + +| Setting | Description | +|---|---| +| WebRTC relay policy | Always relay / Allow direct | +| ICE servers | Custom STUN/TURN server configuration | +| CallKit integration | Enable/disable native iOS call UI | +| Calls in recents | Show/hide calls in Phone app history | +| Lock screen calls | Show/accept on lock screen options | + +### Chat Database Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| Database passphrase & export | `internaldrive` (orange if unencrypted) | `DatabaseView` | Passphrase management, export/import database, file storage stats | +| Migrate to another device | `tray.and.arrow.up` | `MigrateFromDevice` | Export database and generate migration link | + +Database row shows exclamation octagon icon in red when `chatRunning == false`. + +### Help Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| How to use it | `questionmark` | `ChatHelp` | Usage guide with user's display name | +| What's new | `plus` | `WhatsNewView` | Changelog and new features | +| About SimpleX Chat | `info` | `SimpleXInfo` | About page with privacy explanation | +| Send questions and ideas | `number` | Opens SimpleX team chat link | Direct contact with developers | +| Send us email | `envelope` | `mailto:chat@simplex.chat` | Email link | + +### Support SimpleX Chat Section + +| Row | Icon | Action | +|---|---|---| +| Contribute | `keyboard` | Opens GitHub contribution guide | +| Rate the app | `star` | `SKStoreReviewController.requestReview` | +| Star on GitHub | GitHub icon | Opens GitHub repository | + +### Develop Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| Developer tools | `chevron.left.forwardslash.chevron.right` | `DeveloperView` | Chat console/terminal, log level, confirm DB upgrades | +| App version | (none) | `VersionView` | Shows "v{version} ({build})" | + +## Loading / Error States + +| State | Behavior | +|---|---| +| Chat not running | Most navigation links disabled; database row shows warning | +| Database not encrypted | Database icon shown in orange | +| Migration in progress | `showProgress` overlays `ProgressView` on entire settings view | +| Terminal cleanup | On disappear: `chatModel.showingTerminal = false`, terminal items cleared | + +## App Defaults + +Key `UserDefaults` / `AppStorage` keys managed by settings: +- `DEFAULT_PERFORM_LA`, `DEFAULT_LA_MODE`, `DEFAULT_LA_LOCK_DELAY`, `DEFAULT_LA_SELF_DESTRUCT` +- `DEFAULT_PRIVACY_ACCEPT_IMAGES`, `DEFAULT_PRIVACY_LINK_PREVIEWS`, `DEFAULT_PRIVACY_PROTECT_SCREEN` +- `DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS`, `DEFAULT_PRIVACY_SAVE_LAST_DRAFT` +- `DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET`, `DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS` +- `DEFAULT_WEBRTC_POLICY_RELAY`, `DEFAULT_WEBRTC_ICE_SERVERS`, `DEFAULT_CALL_KIT_CALLS_IN_RECENTS` +- `DEFAULT_CURRENT_THEME`, `DEFAULT_SYSTEM_DARK_THEME`, `DEFAULT_THEME_OVERRIDES` +- `DEFAULT_PROFILE_IMAGE_CORNER_RADIUS`, `DEFAULT_CHAT_ITEM_ROUNDNESS`, `DEFAULT_CHAT_ITEM_TAIL` +- `DEFAULT_TOOLBAR_MATERIAL`, `DEFAULT_ONE_HAND_UI_CARD_SHOWN` +- `DEFAULT_DEVELOPER_TOOLS`, `DEFAULT_SHOW_SENT_VIA_RPOXY`, `DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE` + +## Related Specs + +- `spec/architecture.md` -- App architecture overview +- `spec/services/theme.md` -- Theme system specification +- [Chat List](chat-list.md) -- Parent view via UserPicker +- [User Profiles](user-profiles.md) -- Profile management (separate UserPicker option) + +## Source Files + +- `Shared/Views/UserSettings/SettingsView.swift` -- Main settings view, section layout, app defaults definitions +- `Shared/Views/UserSettings/NotificationsView.swift` -- Notification mode and preview settings +- `Shared/Views/UserSettings/AppearanceSettings.swift` -- Theme, wallpaper, UI customization +- `Shared/Views/UserSettings/PrivacySettings.swift` -- Privacy and security settings +- `Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift` -- Server and network configuration +- `Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift` -- TCP/timeout settings +- `Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift` -- SMP/XFTP server list +- `Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift` -- Individual server edit +- `Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift` -- Add new server +- `Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift` -- Scan server QR code +- `Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift` -- Server operator configuration +- `Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift` -- Operator conditions display diff --git a/apps/ios/product/views/user-profiles.md b/apps/ios/product/views/user-profiles.md new file mode 100644 index 0000000000..5a38db1816 --- /dev/null +++ b/apps/ios/product/views/user-profiles.md @@ -0,0 +1,137 @@ +# User Profiles + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/state.md](../../spec/state.md) + +## Purpose + +Manage multiple chat profiles within a single app instance. Users can create, switch between, hide, mute, and delete profiles. Hidden profiles are protected by password and support a self-destruct password option. + +## Route / Navigation + +- **Entry point**: Tap user avatar in `ChatListView` toolbar -> `UserPicker` -> "Your chat profiles" +- **Presented by**: `UserPickerSheetView(sheet: .chatProfiles)` wrapping `UserProfilesView` in a `NavigationView` +- **Navigation title**: "Your chat profiles" +- **Sub-navigation**: + - Create profile -> `CreateProfile` + - Edit profile -> profile detail view (via `selectedUser`) + - User address -> `UserAddressView` (via UserPicker `.address` sheet) + +## Page Sections + +### Search / Password Field + +Combined text field at the top (`searchTextOrPassword`): +- In normal mode: Filters visible profiles by name +- For hidden profiles: Acts as password entry to reveal hidden profiles +- Trimmed search text compared against profile names and hidden profile passwords + +### Profile List + +Each row rendered by `userView()`: + +| Element | Description | +|---|---| +| Active indicator | Checkmark or highlighted state for the current active profile | +| Profile image | Avatar circle with profile image or colored initials | +| Display name | Profile's display name | +| Unread count | Badge showing unread message count across all chats for this profile | +| Muted indicator | Bell-slash icon if profile notifications are muted | +| Hidden indicator | Lock icon for hidden profiles (only shown when revealed via password) | + +### Profile Actions + +Available via tap on a profile row: + +| Action | Condition | Description | +|---|---|---| +| Switch active | Different from current | Activates the selected profile; all chats switch context | +| Mute / Unmute | Any profile | Toggle notification muting for the profile; shows alert on first mute (`showMuteProfileAlert`) | +| Hide / Unhide | Non-active profile | Hide with password or reveal a hidden profile | +| Delete | Non-active profile | Delete with confirmation; option to delete data from servers | + +### Add Profile Button + +| Element | Description | +|---|---| +| "Add profile" label | `Label("Add profile", systemImage: "plus")` | +| Navigation | `NavigationLink` to `CreateProfile` view | +| Auth required | Requires local authentication before creating | + +Only shown when `trimmedSearchTextOrPassword` is empty (not searching/entering password). + +### Hidden Profile Banner + +Shown when `profileHidden` is true (a profile was just hidden): + +| Element | Description | +|---|---| +| Lock icon | `lock.open` system image | +| Message | "Enter password above to show!" | +| Tap action | Dismisses the banner with animation | + +### Create Profile (`CreateProfile`) + +| Field | Description | +|---|---| +| Display name | Required text field with validation (`mkValidName`) | +| Bio | Optional bio text (max 160 bytes) | +| Create button | Disabled until valid name entered and bio within limit | + +Validation alerts: `duplicateUserError`, `invalidDisplayNameError`, `createUserError`, `invalidNameError`. + +## Profile Visibility + +| Visibility | Description | +|---|---| +| Public | Normal profile, always visible in the list | +| Hidden | Protected by password; not shown unless password entered in search field | +| Muted | Notifications suppressed; visual indicator in profile list | + +### Hidden Profile Password Management + +- Set password when hiding a profile +- Password verified when entering in the search/password field +- `UserProfileAction.unhideUser` requires password entry +- Self-destruct password: Optional secondary password (`DEFAULT_LA_SELF_DESTRUCT`) that wipes all app data when entered + +### Delete Profile + +Two-stage confirmation: + +1. `confirmDeleteUser()` shows initial confirmation +2. `UserProfilesAlert.deleteUser(user:, delSMPQueues:)` with option to delete queues from servers +3. Requires local authentication (`withAuth`) before proceeding + +## Loading / Error States + +| State | Behavior | +|---|---| +| Authentication required | `authorized` state; prompts biometric/passcode before profile operations | +| Profile switch | Async operation; profile switch errors shown via `activateUserError` alert | +| Delete in progress | Profile removed from list; server queue deletion is async | +| Errors | Alert with localized error title and description | + +## Alerts + +| Alert | Trigger | +|---|---| +| `deleteUser` | Confirm profile deletion | +| `hiddenProfilesNotice` | First-time hidden profiles explanation (`showHiddenProfilesNotice`) | +| `muteProfileAlert` | First-time mute explanation (`showMuteProfileAlert`) | +| `activateUserError` | Profile switch failure | +| `error` | General error display | + +## Related Specs + +- `spec/api.md` -- User management API commands (create user, delete user, activate user, hide user) +- `spec/state.md` -- Application state: `chatModel.users`, `chatModel.currentUser` +- [Chat List](chat-list.md) -- Reflects active profile's chats +- [Settings](settings.md) -- Accessed from same UserPicker menu +- [Onboarding](onboarding.md) -- Initial profile creation during first launch + +## Source Files + +- `Shared/Views/UserSettings/UserProfilesView.swift` -- Main profiles list, search/password, profile actions, delete confirmation +- `Shared/Views/Onboarding/CreateProfile.swift` -- Profile creation form (shared with onboarding and profiles view) +- `Shared/Views/UserSettings/UserAddressView.swift` -- User's SimpleX address management (create, share, delete) +- `Shared/Views/ChatList/UserPicker.swift` -- Profile switcher sheet that navigates to this view diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 0826bca4a3..87a47ec2ab 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -164,7 +164,7 @@ "%d file(s) were not downloaded." = "%d файлов не было загружено."; /* time interval */ -"%d hours" = "%d час."; +"%d hours" = "%d ч."; /* alert title */ "%d messages not forwarded" = "%d сообщений не переслано"; @@ -729,7 +729,7 @@ swipe action */ "attempts" = "попытки"; /* No comment provided by engineer. */ -"Audio & video calls" = "Аудио- и видеозвонки"; +"Audio & video calls" = "Аудио и видеозвонки"; /* No comment provided by engineer. */ "Audio and video calls" = "Аудио и видео звонки"; @@ -1733,7 +1733,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Удалить сообщение?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Удалить сообщения"; /* No comment provided by engineer. */ @@ -2238,6 +2239,9 @@ chat item action */ /* alert message */ "Error connecting to forwarding server %@. Please try later." = "Ошибка подключения к пересылающему серверу %@. Попробуйте позже."; +/* subscription status explanation */ +"Error connecting to the server used to receive messages from this connection: %@" = "Ошибка подключения к серверу, используемому для получения сообщений от этого соединения: %@"; + /* No comment provided by engineer. */ "Error creating address" = "Ошибка при создании адреса"; @@ -3305,10 +3309,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена будет изменена на \"%@\". Будет отправлено новое приглашение."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "Член будет удален из разговора - это действие нельзя отменить!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!"; /* alert message */ @@ -3710,6 +3714,9 @@ snd error text */ /* servers error */ "No servers to send files." = "Нет серверов для отправки файлов."; +/* No comment provided by engineer. */ +"no subscription" = "нет подписки"; + /* copied message info in history */ "no text" = "нет текста"; @@ -4374,7 +4381,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Relay сервер защищает Ваш IP адрес, но может отслеживать продолжительность звонка."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Удалить"; /* No comment provided by engineer. */ @@ -4389,7 +4396,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Удалить члена группы"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Удалить члена группы?"; /* No comment provided by engineer. */ @@ -5545,7 +5552,7 @@ report reason */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**."; /* No comment provided by engineer. */ -"To send" = "Для оправки"; +"To send" = "Для отправки"; /* alert message */ "To send commands you must be connected." = "Вы должны быть соединены, чтобы отправлять команды."; @@ -5583,6 +5590,9 @@ report reason */ /* No comment provided by engineer. */ "Transport sessions" = "Транспортные сессии"; +/* subscription status explanation */ +"Trying to connect to the server used to receive messages from this connection." = "Попытка подключиться к серверу, используемому для получения сообщений от этого соединения."; + /* No comment provided by engineer. */ "Turkish interface" = "Турецкий интерфейс"; @@ -6075,9 +6085,15 @@ report reason */ /* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Вы уже вступаете в группу!\nПовторить запрос на вступление?"; +/* subscription status explanation */ +"You are connected to the server used to receive messages from this connection." = "Вы подключены к серверу, используемому для приема сообщений от этого соединения."; + /* No comment provided by engineer. */ "You are invited to group" = "Вы приглашены в группу"; +/* subscription status explanation */ +"You are not connected to the server used to receive messages from this connection (no subscription)." = "Вы не подключены к серверу, используемому для получения сообщений по этому соединению (нет подписки)."; + /* No comment provided by engineer. */ "You are not connected to these servers. Private routing is used to deliver messages to them." = "Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка."; diff --git a/apps/ios/spec/README.md b/apps/ios/spec/README.md new file mode 100644 index 0000000000..eca6103582 --- /dev/null +++ b/apps/ios/spec/README.md @@ -0,0 +1,74 @@ +# SimpleX Chat iOS -- Specification Overview + +> Technical specification suite for the SimpleX Chat iOS application. Each document provides bidirectional links to product documentation and source code. + +## Executive Summary + +The SimpleX Chat iOS app is a native SwiftUI frontend that communicates with a Haskell core library via C FFI. All chat logic, encryption, protocol handling, and database operations happen in the Haskell core (`chat_ctrl`). The iOS layer handles UI rendering, system integration (CallKit, Push Notifications, Background Tasks), local preferences, and theming. The app shares its database with a Notification Service Extension (NSE) for decrypting push payloads while the main app is inactive. + +## Dependency Graph + +``` +SimpleXApp (root entry point) +├── ChatModel (ObservableObject state) <-> SimpleXAPI (FFI bridge) <-> Haskell Core (chat_ctrl) +├── Views (SwiftUI) +│ ├── ChatListView -> ChatView -> ComposeView +│ ├── ChatItemView (renders individual messages) +│ ├── Settings, UserProfiles, Onboarding +│ └── ActiveCallView (WebRTC + CallKit) +├── Models +│ ├── ChatModel (global app state -- singleton) +│ ├── ItemsModel (per-chat message list state -- singleton + secondary instances) +│ ├── ChatTagsModel (tag filtering state) +│ └── Chat (per-conversation observable state) +├── Services +│ ├── NtfManager (push notification coordination) +│ ├── BGManager (background task scheduling) +│ ├── CallController (CallKit + VoIP push) +│ └── ThemeManager (theme resolution engine) +└── Extensions + ├── SimpleX NSE (Notification Service Extension -- decrypts push payloads) + └── SimpleX SE (Share Extension) +``` + +## Specification Documents + +| Document | Description | +|----------|-------------| +| [Architecture](architecture.md) | System architecture, FFI bridge, app lifecycle, extension model | +| [Chat API Reference](api.md) | Complete ChatCommand, ChatResponse, ChatEvent, ChatError type reference | +| [State Management](state.md) | ChatModel, ItemsModel, Chat, ChatInfo, preference storage | +| [Database & Storage](database.md) | SQLite databases, encryption, file storage, export/import | +| [Chat View](client/chat-view.md) | Message rendering, chat item types, context menu actions | +| [Chat List](client/chat-list.md) | Conversation list, filtering, search, swipe actions | +| [Message Composition](client/compose.md) | Compose bar, attachments, reply/edit/forward modes, voice recording | +| [Navigation](client/navigation.md) | Navigation stack, deep linking, sheet presentation, call overlay | +| [Push Notifications](services/notifications.md) | NtfManager, NSE, notification modes, token lifecycle | +| [WebRTC Calling](services/calls.md) | CallController, WebRTCClient, CallKit, signaling via SMP | +| [File Transfer](services/files.md) | Inline/XFTP transfer, auto-receive, CryptoFile, file constants | +| [Theme Engine](services/theme.md) | ThemeManager, default themes, customization layers, wallpapers | +| [Impact Graph](impact.md) | Source file → product concept mapping, risk levels | + +## Related Product Documentation + +- [Product Overview](../product/README.md) +- [Concept Index](../product/concepts.md) +- [Business Rules](../product/rules.md) +- [Known Gaps](../product/gaps.md) +- [Glossary](../product/glossary.md) +- [Chat List View](../product/views/chat-list.md) +- [Chat View](../product/views/chat.md) + +## Source Code Entry Points + +| File | Role | +|------|------| +| `Shared/SimpleXApp.swift` | App entry point, Haskell init, lifecycle management | +| `Shared/AppDelegate.swift` | UIApplicationDelegate for push token registration | +| `Shared/ContentView.swift` | Root view -- authentication gate, call overlay, navigation | +| `Shared/Model/ChatModel.swift` | Primary observable state (ChatModel, ItemsModel, Chat) | +| `Shared/Model/SimpleXAPI.swift` | FFI bridge -- chatSendCmd, chatApiSendCmd, sendSimpleXCmd | +| `Shared/Model/AppAPITypes.swift` | ChatCommand, ChatResponse, ChatEvent enums (iOS app layer) | +| `SimpleXChat/APITypes.swift` | APIResult, ChatError, ChatCmdProtocol (shared framework) | +| `SimpleXChat/ChatTypes.swift` | User, ChatInfo, Contact, GroupInfo, ChatItem data types | +| `SimpleXChat/SimpleX.h` | C header for Haskell FFI functions | diff --git a/apps/ios/spec/api.md b/apps/ios/spec/api.md new file mode 100644 index 0000000000..fe40a4c4ec --- /dev/null +++ b/apps/ios/spec/api.md @@ -0,0 +1,600 @@ +# SimpleX Chat iOS -- Chat API Reference + +> Complete specification of the ChatCommand, ChatResponse, ChatEvent, and ChatError types that form the API between the Swift UI layer and the Haskell core. +> +> Related specs: [Architecture](architecture.md) | [State Management](state.md) | [README](README.md) +> Related product: [Concept Index](../product/concepts.md) + +**Source:** [`AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift) | [`SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift) | [`APITypes.swift`](../SimpleXChat/APITypes.swift) | [`API.swift`](../SimpleXChat/API.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Command Categories (ChatCommand)](#2-command-categories) +3. [Response Types (ChatResponse)](#3-response-types) +4. [Event Types (ChatEvent)](#4-event-types) +5. [Error Types (ChatError)](#5-error-types) +6. [FFI Bridge Functions](#6-ffi-bridge-functions) +7. [Result Type (APIResult)](#7-result-type) + +--- + +## 1. Overview + +The iOS app communicates with the Haskell core exclusively through a command/response protocol: + +1. Swift constructs a `ChatCommand` enum value +2. The command's `cmdString` property serializes it to a text command +3. The FFI bridge sends the string to Haskell via `chat_send_cmd_retry` +4. Haskell returns a JSON response, decoded as `APIResult` +5. Async events arrive separately via `chat_recv_msg_wait`, decoded as `ChatEvent` + +**Source files**: +- [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift) -- `ChatCommand` ([L14](../Shared/Model/AppAPITypes.swift#L15)), `ChatResponse0` ([L647](../Shared/Model/AppAPITypes.swift#L649)), `ChatResponse1` ([L768](../Shared/Model/AppAPITypes.swift#L771)), `ChatResponse2` ([L907](../Shared/Model/AppAPITypes.swift#L911)), `ChatEvent` ([L1050](../Shared/Model/AppAPITypes.swift#L1055)) enums +- [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift) -- `APIResult` ([L26](../SimpleXChat/APITypes.swift#L27)), `ChatAPIResult` ([L63](../SimpleXChat/APITypes.swift#L65)), `ChatError` ([L695](../SimpleXChat/APITypes.swift#L699)) +- [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift) -- FFI bridge functions (`chatSendCmd` [L117](../Shared/Model/SimpleXAPI.swift#L121), `chatRecvMsg` [L230](../Shared/Model/SimpleXAPI.swift#L237)) +- [`SimpleXChat/API.swift`](../SimpleXChat/API.swift) -- Low-level FFI (`sendSimpleXCmd` [L114](../SimpleXChat/API.swift#L115), `recvSimpleXMsg` [L136](../SimpleXChat/API.swift#L137)) +- `SimpleXChat/ChatTypes.swift` -- Data types used in commands/responses (User, Contact, GroupInfo, ChatItem, etc.) +- `../../src/Simplex/Chat/Controller.hs` -- Haskell controller (function `chat_send_cmd_retry`, `chat_recv_msg_wait`) + +--- + +## 2. Command Categories + +The `ChatCommand` enum ([`AppAPITypes.swift` L14](../Shared/Model/AppAPITypes.swift#L15)) contains all commands the iOS app can send to the Haskell core. Commands are organized below by functional area. + +### 2.1 User Management + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `showActiveUser` | -- | Get current active user | [L15](../Shared/Model/AppAPITypes.swift#L16) | +| `createActiveUser` | `profile: Profile?, pastTimestamp: Bool` | Create new user profile | [L16](../Shared/Model/AppAPITypes.swift#L17) | +| `listUsers` | -- | List all user profiles | [L17](../Shared/Model/AppAPITypes.swift#L18) | +| `apiSetActiveUser` | `userId: Int64, viewPwd: String?` | Switch active user | [L18](../Shared/Model/AppAPITypes.swift#L19) | +| `apiHideUser` | `userId: Int64, viewPwd: String` | Hide user behind password | [L23](../Shared/Model/AppAPITypes.swift#L24) | +| `apiUnhideUser` | `userId: Int64, viewPwd: String` | Unhide hidden user | [L24](../Shared/Model/AppAPITypes.swift#L25) | +| `apiMuteUser` | `userId: Int64` | Mute notifications for user | [L25](../Shared/Model/AppAPITypes.swift#L26) | +| `apiUnmuteUser` | `userId: Int64` | Unmute notifications for user | [L26](../Shared/Model/AppAPITypes.swift#L27) | +| `apiDeleteUser` | `userId: Int64, delSMPQueues: Bool, viewPwd: String?` | Delete user profile | [L27](../Shared/Model/AppAPITypes.swift#L28) | +| `apiUpdateProfile` | `userId: Int64, profile: Profile` | Update user display name/image | [L138](../Shared/Model/AppAPITypes.swift#L139) | +| `setAllContactReceipts` | `enable: Bool` | Set delivery receipts for all contacts | [L19](../Shared/Model/AppAPITypes.swift#L20) | +| `apiSetUserContactReceipts` | `userId: Int64, userMsgReceiptSettings` | Per-user contact receipt settings | [L20](../Shared/Model/AppAPITypes.swift#L21) | +| `apiSetUserGroupReceipts` | `userId: Int64, userMsgReceiptSettings` | Per-user group receipt settings | [L21](../Shared/Model/AppAPITypes.swift#L22) | +| `apiSetUserAutoAcceptMemberContacts` | `userId: Int64, enable: Bool` | Auto-accept group member contacts | [L22](../Shared/Model/AppAPITypes.swift#L23) | + +### 2.2 Chat Lifecycle Control + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `startChat` | `mainApp: Bool, enableSndFiles: Bool` | Start chat engine | [L28](../Shared/Model/AppAPITypes.swift#L29) | +| `checkChatRunning` | -- | Check if chat is running | [L29](../Shared/Model/AppAPITypes.swift#L30) | +| `apiStopChat` | -- | Stop chat engine | [L30](../Shared/Model/AppAPITypes.swift#L31) | +| `apiActivateChat` | `restoreChat: Bool` | Resume from background | [L31](../Shared/Model/AppAPITypes.swift#L32) | +| `apiSuspendChat` | `timeoutMicroseconds: Int` | Suspend for background | [L32](../Shared/Model/AppAPITypes.swift#L33) | +| `apiSetAppFilePaths` | `filesFolder, tempFolder, assetsFolder` | Set file storage paths | [L33](../Shared/Model/AppAPITypes.swift#L34) | +| `apiSetEncryptLocalFiles` | `enable: Bool` | Toggle local file encryption | [L34](../Shared/Model/AppAPITypes.swift#L35) | + +### 2.3 Chat & Message Operations + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiGetChats` | `userId: Int64` | Get all chat previews for user | [L43](../Shared/Model/AppAPITypes.swift#L44) | +| `apiGetChat` | `chatId, scope, contentTag, pagination, search` | Get messages for a chat | [L44](../Shared/Model/AppAPITypes.swift#L45) | +| `apiGetChatContentTypes` | `chatId, scope` | Get content type counts for a chat | [L45](../Shared/Model/AppAPITypes.swift#L46) | +| `apiGetChatItemInfo` | `type, id, scope, itemId` | Get detailed info for a message | [L46](../Shared/Model/AppAPITypes.swift#L47) | +| `apiSendMessages` | `type, id, scope, live, ttl, composedMessages` | Send one or more messages | [L47](../Shared/Model/AppAPITypes.swift#L48) | +| `apiCreateChatItems` | `noteFolderId, composedMessages` | Create items in notes folder | [L53](../Shared/Model/AppAPITypes.swift#L54) | +| `apiUpdateChatItem` | `type, id, scope, itemId, updatedMessage, live` | Edit a sent message | [L55](../Shared/Model/AppAPITypes.swift#L56) | +| `apiDeleteChatItem` | `type, id, scope, itemIds, mode` | Delete messages | [L56](../Shared/Model/AppAPITypes.swift#L57) | +| `apiDeleteMemberChatItem` | `groupId, itemIds` | Moderate group messages | [L57](../Shared/Model/AppAPITypes.swift#L58) | +| `apiChatItemReaction` | `type, id, scope, itemId, add, reaction` | Add/remove emoji reaction | [L60](../Shared/Model/AppAPITypes.swift#L61) | +| `apiGetReactionMembers` | `userId, groupId, itemId, reaction` | Get who reacted | [L61](../Shared/Model/AppAPITypes.swift#L62) | +| `apiPlanForwardChatItems` | `fromChatType, fromChatId, fromScope, itemIds` | Plan message forwarding | [L62](../Shared/Model/AppAPITypes.swift#L63) | +| `apiForwardChatItems` | `toChatType, toChatId, toScope, from..., itemIds, ttl` | Forward messages | [L63](../Shared/Model/AppAPITypes.swift#L64) | +| `apiReportMessage` | `groupId, chatItemId, reportReason, reportText` | Report group message | [L54](../Shared/Model/AppAPITypes.swift#L55) | +| `apiChatRead` | `type, id, scope` | Mark entire chat as read | [L163](../Shared/Model/AppAPITypes.swift#L164) | +| `apiChatItemsRead` | `type, id, scope, itemIds` | Mark specific items as read | [L164](../Shared/Model/AppAPITypes.swift#L165) | +| `apiChatUnread` | `type, id, unreadChat` | Toggle unread badge | [L165](../Shared/Model/AppAPITypes.swift#L166) | + +### 2.4 Contact Management + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiAddContact` | `userId, incognito` | Create invitation link | [L123](../Shared/Model/AppAPITypes.swift#L124) | +| `apiConnect` | `userId, incognito, connLink` | Connect via link | [L133](../Shared/Model/AppAPITypes.swift#L134) | +| `apiConnectPlan` | `userId, connLink` | Plan connection (preview) | [L126](../Shared/Model/AppAPITypes.swift#L127) | +| `apiPrepareContact` | `userId, connLink, contactShortLinkData` | Prepare contact from link | [L127](../Shared/Model/AppAPITypes.swift#L128) | +| `apiConnectPreparedContact` | `contactId, incognito, msg` | Connect prepared contact | [L131](../Shared/Model/AppAPITypes.swift#L132) | +| `apiConnectContactViaAddress` | `userId, incognito, contactId` | Connect via address | [L134](../Shared/Model/AppAPITypes.swift#L135) | +| `apiAcceptContact` | `incognito, contactReqId` | Accept contact request | [L151](../Shared/Model/AppAPITypes.swift#L152) | +| `apiRejectContact` | `contactReqId` | Reject contact request | [L152](../Shared/Model/AppAPITypes.swift#L153) | +| `apiDeleteChat` | `type, id, chatDeleteMode` | Delete conversation | [L135](../Shared/Model/AppAPITypes.swift#L136) | +| `apiClearChat` | `type, id` | Clear conversation history | [L136](../Shared/Model/AppAPITypes.swift#L137) | +| `apiListContacts` | `userId` | List all contacts | [L137](../Shared/Model/AppAPITypes.swift#L138) | +| `apiSetContactPrefs` | `contactId, preferences` | Set contact preferences | [L139](../Shared/Model/AppAPITypes.swift#L140) | +| `apiSetContactAlias` | `contactId, localAlias` | Set local alias | [L140](../Shared/Model/AppAPITypes.swift#L141) | +| `apiSetConnectionAlias` | `connId, localAlias` | Set pending connection alias | [L142](../Shared/Model/AppAPITypes.swift#L143) | +| `apiContactInfo` | `contactId` | Get contact info + connection stats | [L109](../Shared/Model/AppAPITypes.swift#L110) | +| `apiSetConnectionIncognito` | `connId, incognito` | Toggle incognito on pending connection | [L124](../Shared/Model/AppAPITypes.swift#L125) | + +### 2.5 Group Management + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiNewGroup` | `userId, incognito, groupProfile` | Create new group | [L71](../Shared/Model/AppAPITypes.swift#L72) | +| `apiAddMember` | `groupId, contactId, memberRole` | Invite contact to group | [L72](../Shared/Model/AppAPITypes.swift#L73) | +| `apiJoinGroup` | `groupId` | Accept group invitation | [L73](../Shared/Model/AppAPITypes.swift#L74) | +| `apiAcceptMember` | `groupId, groupMemberId, memberRole` | Accept member (knocking) | [L74](../Shared/Model/AppAPITypes.swift#L75) | +| `apiRemoveMembers` | `groupId, memberIds, withMessages` | Remove members | [L78](../Shared/Model/AppAPITypes.swift#L79) | +| `apiLeaveGroup` | `groupId` | Leave group | [L79](../Shared/Model/AppAPITypes.swift#L80) | +| `apiListMembers` | `groupId` | List group members | [L80](../Shared/Model/AppAPITypes.swift#L81) | +| `apiUpdateGroupProfile` | `groupId, groupProfile` | Update group name/image/description | [L81](../Shared/Model/AppAPITypes.swift#L82) | +| `apiMembersRole` | `groupId, memberIds, memberRole` | Change member roles | [L76](../Shared/Model/AppAPITypes.swift#L77) | +| `apiBlockMembersForAll` | `groupId, memberIds, blocked` | Block members for all | [L77](../Shared/Model/AppAPITypes.swift#L78) | +| `apiCreateGroupLink` | `groupId, memberRole` | Create shareable group link | [L82](../Shared/Model/AppAPITypes.swift#L83) | +| `apiGroupLinkMemberRole` | `groupId, memberRole` | Change group link default role | [L83](../Shared/Model/AppAPITypes.swift#L84) | +| `apiDeleteGroupLink` | `groupId` | Delete group link | [L84](../Shared/Model/AppAPITypes.swift#L85) | +| `apiGetGroupLink` | `groupId` | Get existing group link | [L85](../Shared/Model/AppAPITypes.swift#L86) | +| `apiAddGroupShortLink` | `groupId` | Add short link to group | [L86](../Shared/Model/AppAPITypes.swift#L87) | +| `apiCreateMemberContact` | `groupId, groupMemberId` | Create direct contact from group member | [L87](../Shared/Model/AppAPITypes.swift#L88) | +| `apiSendMemberContactInvitation` | `contactId, msg` | Send contact invitation to member | [L88](../Shared/Model/AppAPITypes.swift#L89) | +| `apiGroupMemberInfo` | `groupId, groupMemberId` | Get member info + connection stats | [L110](../Shared/Model/AppAPITypes.swift#L111) | +| `apiDeleteMemberSupportChat` | `groupId, groupMemberId` | Delete member support chat | [L75](../Shared/Model/AppAPITypes.swift#L76) | +| `apiSetMemberSettings` | `groupId, groupMemberId, memberSettings` | Set per-member settings | [L108](../Shared/Model/AppAPITypes.swift#L109) | +| `apiSetGroupAlias` | `groupId, localAlias` | Set local group alias | [L141](../Shared/Model/AppAPITypes.swift#L142) | + +### 2.6 Chat Tags + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiGetChatTags` | `userId` | Get all user tags | [L42](../Shared/Model/AppAPITypes.swift#L43) | +| `apiCreateChatTag` | `tag: ChatTagData` | Create a new tag | [L48](../Shared/Model/AppAPITypes.swift#L49) | +| `apiSetChatTags` | `type, id, tagIds` | Assign tags to a chat | [L49](../Shared/Model/AppAPITypes.swift#L50) | +| `apiDeleteChatTag` | `tagId` | Delete a tag | [L50](../Shared/Model/AppAPITypes.swift#L51) | +| `apiUpdateChatTag` | `tagId, tagData` | Update tag name/emoji | [L51](../Shared/Model/AppAPITypes.swift#L52) | +| `apiReorderChatTags` | `tagIds` | Reorder tags | [L52](../Shared/Model/AppAPITypes.swift#L53) | + +### 2.7 File Operations + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `receiveFile` | `fileId, userApprovedRelays, encrypted, inline` | Accept and download file | [L166](../Shared/Model/AppAPITypes.swift#L167) | +| `setFileToReceive` | `fileId, userApprovedRelays, encrypted` | Mark file for auto-receive | [L167](../Shared/Model/AppAPITypes.swift#L168) | +| `cancelFile` | `fileId` | Cancel file transfer | [L168](../Shared/Model/AppAPITypes.swift#L169) | +| `apiUploadStandaloneFile` | `userId, file: CryptoFile` | Upload file to XFTP (no chat) | [L178](../Shared/Model/AppAPITypes.swift#L179) | +| `apiDownloadStandaloneFile` | `userId, url, file: CryptoFile` | Download from XFTP URL | [L179](../Shared/Model/AppAPITypes.swift#L180) | +| `apiStandaloneFileInfo` | `url` | Get file metadata from XFTP URL | [L180](../Shared/Model/AppAPITypes.swift#L181) | + +### 2.8 WebRTC Call Operations + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiSendCallInvitation` | `contact, callType` | Initiate call | [L154](../Shared/Model/AppAPITypes.swift#L155) | +| `apiRejectCall` | `contact` | Reject incoming call | [L155](../Shared/Model/AppAPITypes.swift#L156) | +| `apiSendCallOffer` | `contact, callOffer: WebRTCCallOffer` | Send SDP offer | [L156](../Shared/Model/AppAPITypes.swift#L157) | +| `apiSendCallAnswer` | `contact, answer: WebRTCSession` | Send SDP answer | [L157](../Shared/Model/AppAPITypes.swift#L158) | +| `apiSendCallExtraInfo` | `contact, extraInfo: WebRTCExtraInfo` | Send ICE candidates | [L158](../Shared/Model/AppAPITypes.swift#L159) | +| `apiEndCall` | `contact` | End active call | [L159](../Shared/Model/AppAPITypes.swift#L160) | +| `apiGetCallInvitations` | -- | Get pending call invitations | [L160](../Shared/Model/AppAPITypes.swift#L161) | +| `apiCallStatus` | `contact, callStatus` | Report call status change | [L161](../Shared/Model/AppAPITypes.swift#L162) | + +### 2.9 Push Notifications + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiGetNtfToken` | -- | Get current notification token | [L64](../Shared/Model/AppAPITypes.swift#L65) | +| `apiRegisterToken` | `token, notificationMode` | Register device token with server | [L65](../Shared/Model/AppAPITypes.swift#L66) | +| `apiVerifyToken` | `token, nonce, code` | Verify token registration | [L66](../Shared/Model/AppAPITypes.swift#L67) | +| `apiCheckToken` | `token` | Check token status | [L67](../Shared/Model/AppAPITypes.swift#L68) | +| `apiDeleteToken` | `token` | Unregister token | [L68](../Shared/Model/AppAPITypes.swift#L69) | +| `apiGetNtfConns` | `nonce, encNtfInfo` | Get notification connections (NSE) | [L69](../Shared/Model/AppAPITypes.swift#L70) | +| `apiGetConnNtfMessages` | `connMsgReqs` | Get notification messages (NSE) | [L70](../Shared/Model/AppAPITypes.swift#L71) | + +### 2.10 Settings & Configuration + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiSaveSettings` | `settings: AppSettings` | Save app settings to core | [L40](../Shared/Model/AppAPITypes.swift#L41) | +| `apiGetSettings` | `settings: AppSettings` | Get settings from core | [L41](../Shared/Model/AppAPITypes.swift#L42) | +| `apiSetChatSettings` | `type, id, chatSettings` | Per-chat notification settings | [L107](../Shared/Model/AppAPITypes.swift#L108) | +| `apiSetChatItemTTL` | `userId, seconds` | Set global message TTL | [L99](../Shared/Model/AppAPITypes.swift#L100) | +| `apiGetChatItemTTL` | `userId` | Get global message TTL | [L100](../Shared/Model/AppAPITypes.swift#L101) | +| `apiSetChatTTL` | `userId, type, id, seconds` | Per-chat message TTL | [L101](../Shared/Model/AppAPITypes.swift#L102) | +| `apiSetNetworkConfig` | `networkConfig: NetCfg` | Set network configuration | [L102](../Shared/Model/AppAPITypes.swift#L103) | +| `apiGetNetworkConfig` | -- | Get network configuration | [L103](../Shared/Model/AppAPITypes.swift#L104) | +| `apiSetNetworkInfo` | `networkInfo: UserNetworkInfo` | Set network type/status | [L104](../Shared/Model/AppAPITypes.swift#L105) | +| `reconnectAllServers` | -- | Force reconnect all servers | [L105](../Shared/Model/AppAPITypes.swift#L106) | +| `reconnectServer` | `userId, smpServer` | Reconnect specific server | [L106](../Shared/Model/AppAPITypes.swift#L107) | + +### 2.11 Database & Storage + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiStorageEncryption` | `config: DBEncryptionConfig` | Set/change database encryption | [L38](../Shared/Model/AppAPITypes.swift#L39) | +| `testStorageEncryption` | `key: String` | Test encryption key | [L39](../Shared/Model/AppAPITypes.swift#L40) | +| `apiExportArchive` | `config: ArchiveConfig` | Export database archive | [L35](../Shared/Model/AppAPITypes.swift#L36) | +| `apiImportArchive` | `config: ArchiveConfig` | Import database archive | [L36](../Shared/Model/AppAPITypes.swift#L37) | +| `apiDeleteStorage` | -- | Delete all storage | [L37](../Shared/Model/AppAPITypes.swift#L38) | + +### 2.12 Server Operations + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiGetServerOperators` | -- | Get server operators | [L91](../Shared/Model/AppAPITypes.swift#L92) | +| `apiSetServerOperators` | `operators` | Set server operators | [L92](../Shared/Model/AppAPITypes.swift#L93) | +| `apiGetUserServers` | `userId` | Get user's configured servers | [L93](../Shared/Model/AppAPITypes.swift#L94) | +| `apiSetUserServers` | `userId, userServers` | Set user's servers | [L94](../Shared/Model/AppAPITypes.swift#L95) | +| `apiValidateServers` | `userId, userServers` | Validate server configuration | [L95](../Shared/Model/AppAPITypes.swift#L96) | +| `apiGetUsageConditions` | -- | Get usage conditions | [L96](../Shared/Model/AppAPITypes.swift#L97) | +| `apiAcceptConditions` | `conditionsId, operatorIds` | Accept usage conditions | [L98](../Shared/Model/AppAPITypes.swift#L99) | +| `apiTestProtoServer` | `userId, server` | Test server connectivity | [L90](../Shared/Model/AppAPITypes.swift#L91) | + +### 2.13 Theme & UI + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiSetUserUIThemes` | `userId, themes: ThemeModeOverrides?` | Set per-user theme | [L143](../Shared/Model/AppAPITypes.swift#L144) | +| `apiSetChatUIThemes` | `chatId, themes: ThemeModeOverrides?` | Set per-chat theme | [L144](../Shared/Model/AppAPITypes.swift#L145) | + +### 2.14 Remote Desktop + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `setLocalDeviceName` | `displayName` | Set device name for pairing | [L170](../Shared/Model/AppAPITypes.swift#L171) | +| `connectRemoteCtrl` | `xrcpInvitation` | Connect to desktop via QR code | [L171](../Shared/Model/AppAPITypes.swift#L172) | +| `findKnownRemoteCtrl` | -- | Find previously paired desktops | [L172](../Shared/Model/AppAPITypes.swift#L173) | +| `confirmRemoteCtrl` | `remoteCtrlId` | Confirm known remote controller | [L173](../Shared/Model/AppAPITypes.swift#L174) | +| `verifyRemoteCtrlSession` | `sessionCode` | Verify session code | [L174](../Shared/Model/AppAPITypes.swift#L175) | +| `listRemoteCtrls` | -- | List known remote controllers | [L175](../Shared/Model/AppAPITypes.swift#L176) | +| `stopRemoteCtrl` | -- | Stop remote session | [L176](../Shared/Model/AppAPITypes.swift#L177) | +| `deleteRemoteCtrl` | `remoteCtrlId` | Delete known controller | [L177](../Shared/Model/AppAPITypes.swift#L178) | + +### 2.15 Diagnostics + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `showVersion` | -- | Get core version info | [L182](../Shared/Model/AppAPITypes.swift#L183) | +| `getAgentSubsTotal` | `userId` | Get total SMP subscriptions | [L183](../Shared/Model/AppAPITypes.swift#L184) | +| `getAgentServersSummary` | `userId` | Get server summary stats | [L184](../Shared/Model/AppAPITypes.swift#L185) | +| `resetAgentServersStats` | -- | Reset server statistics | [L185](../Shared/Model/AppAPITypes.swift#L186) | + +### 2.16 Address Management + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiCreateMyAddress` | `userId` | Create SimpleX address | [L145](../Shared/Model/AppAPITypes.swift#L146) | +| `apiDeleteMyAddress` | `userId` | Delete SimpleX address | [L146](../Shared/Model/AppAPITypes.swift#L147) | +| `apiShowMyAddress` | `userId` | Show current address | [L147](../Shared/Model/AppAPITypes.swift#L148) | +| `apiAddMyAddressShortLink` | `userId` | Add short link to address | [L148](../Shared/Model/AppAPITypes.swift#L149) | +| `apiSetProfileAddress` | `userId, on: Bool` | Toggle address in profile | [L149](../Shared/Model/AppAPITypes.swift#L150) | +| `apiSetAddressSettings` | `userId, addressSettings` | Configure address settings | [L150](../Shared/Model/AppAPITypes.swift#L151) | + +### 2.17 Connection Security + +| Command | Parameters | Description | Source | +|---------|-----------|-------------|--------| +| `apiGetContactCode` | `contactId` | Get verification code | [L119](../Shared/Model/AppAPITypes.swift#L120) | +| `apiGetGroupMemberCode` | `groupId, groupMemberId` | Get member verification code | [L120](../Shared/Model/AppAPITypes.swift#L121) | +| `apiVerifyContact` | `contactId, connectionCode` | Verify contact identity | [L121](../Shared/Model/AppAPITypes.swift#L122) | +| `apiVerifyGroupMember` | `groupId, groupMemberId, connectionCode` | Verify group member identity | [L122](../Shared/Model/AppAPITypes.swift#L123) | +| `apiSwitchContact` | `contactId` | Switch contact connection (key rotation) | [L113](../Shared/Model/AppAPITypes.swift#L114) | +| `apiSwitchGroupMember` | `groupId, groupMemberId` | Switch group member connection | [L114](../Shared/Model/AppAPITypes.swift#L115) | +| `apiAbortSwitchContact` | `contactId` | Abort contact switch | [L115](../Shared/Model/AppAPITypes.swift#L116) | +| `apiAbortSwitchGroupMember` | `groupId, groupMemberId` | Abort member switch | [L116](../Shared/Model/AppAPITypes.swift#L117) | +| `apiSyncContactRatchet` | `contactId, force` | Sync double-ratchet state | [L117](../Shared/Model/AppAPITypes.swift#L118) | +| `apiSyncGroupMemberRatchet` | `groupId, groupMemberId, force` | Sync member ratchet | [L118](../Shared/Model/AppAPITypes.swift#L119) | + +--- + +## 3. Response Types + +Responses are split across three enums due to Swift enum size limitations: + +### ChatResponse0 + +Synchronous query responses ([`AppAPITypes.swift` L647](../Shared/Model/AppAPITypes.swift#L649)): + +| Response | Key Fields | Description | Source | +|----------|-----------|-------------|--------| +| `activeUser` | `user: User` | Current active user | [L648](../Shared/Model/AppAPITypes.swift#L650) | +| `usersList` | `users: [UserInfo]` | All user profiles | [L649](../Shared/Model/AppAPITypes.swift#L651) | +| `chatStarted` | -- | Chat engine started | [L650](../Shared/Model/AppAPITypes.swift#L652) | +| `chatRunning` | -- | Chat is already running | [L651](../Shared/Model/AppAPITypes.swift#L653) | +| `chatStopped` | -- | Chat engine stopped | [L652](../Shared/Model/AppAPITypes.swift#L654) | +| `apiChats` | `user, chats: [ChatData]` | All chat previews | [L653](../Shared/Model/AppAPITypes.swift#L655) | +| `apiChat` | `user, chat: ChatData, navInfo` | Single chat with messages | [L654](../Shared/Model/AppAPITypes.swift#L656) | +| `chatTags` | `user, userTags: [ChatTag]` | User's chat tags | [L656](../Shared/Model/AppAPITypes.swift#L658) | +| `chatItemInfo` | `user, chatItem, chatItemInfo` | Message detail info | [L657](../Shared/Model/AppAPITypes.swift#L659) | +| `serverTestResult` | `user, testServer, testFailure` | Server test result | [L658](../Shared/Model/AppAPITypes.swift#L660) | +| `networkConfig` | `networkConfig: NetCfg` | Current network config | [L664](../Shared/Model/AppAPITypes.swift#L666) | +| `contactInfo` | `user, contact, connectionStats, customUserProfile` | Contact details | [L665](../Shared/Model/AppAPITypes.swift#L667) | +| `groupMemberInfo` | `user, groupInfo, member, connectionStats` | Member details | [L666](../Shared/Model/AppAPITypes.swift#L668) | +| `connectionVerified` | `verified, expectedCode` | Verification result | [L676](../Shared/Model/AppAPITypes.swift#L678) | +| `tagsUpdated` | `user, userTags, chatTags` | Tags changed | [L677](../Shared/Model/AppAPITypes.swift#L679) | + +### ChatResponse1 + +Contact, message, and profile responses ([`AppAPITypes.swift` L768](../Shared/Model/AppAPITypes.swift#L771)): + +| Response | Key Fields | Description | Source | +|----------|-----------|-------------|--------| +| `invitation` | `user, connLinkInvitation, connection` | Created invitation link | [L769](../Shared/Model/AppAPITypes.swift#L772) | +| `connectionPlan` | `user, connLink, connectionPlan` | Connection plan preview | [L772](../Shared/Model/AppAPITypes.swift#L775) | +| `newPreparedChat` | `user, chat: ChatData` | Prepared contact/group | [L773](../Shared/Model/AppAPITypes.swift#L776) | +| `contactDeleted` | `user, contact` | Contact deleted | [L782](../Shared/Model/AppAPITypes.swift#L785) | +| `newChatItems` | `user, chatItems: [AChatItem]` | New messages sent/received | [L800](../Shared/Model/AppAPITypes.swift#L803) | +| `chatItemUpdated` | `user, chatItem: AChatItem` | Message edited | [L803](../Shared/Model/AppAPITypes.swift#L806) | +| `chatItemReaction` | `user, added, reaction` | Reaction change | [L805](../Shared/Model/AppAPITypes.swift#L808) | +| `chatItemsDeleted` | `user, chatItemDeletions, byUser` | Messages deleted | [L807](../Shared/Model/AppAPITypes.swift#L810) | +| `contactsList` | `user, contacts: [Contact]` | All contacts list | [L808](../Shared/Model/AppAPITypes.swift#L811) | +| `userProfileUpdated` | `user, fromProfile, toProfile` | Profile changed | [L788](../Shared/Model/AppAPITypes.swift#L791) | +| `userContactLinkCreated` | `user, connLinkContact` | Address created | [L796](../Shared/Model/AppAPITypes.swift#L799) | +| `forwardPlan` | `user, chatItemIds, forwardConfirmation` | Forward plan result | [L802](../Shared/Model/AppAPITypes.swift#L805) | +| `groupChatItemsDeleted` | `user, groupInfo, chatItemIDs, byUser, member_` | Group items deleted | [L801](../Shared/Model/AppAPITypes.swift#L804) | + +### ChatResponse2 + +Group, file, call, notification, and misc responses ([`AppAPITypes.swift` L907](../Shared/Model/AppAPITypes.swift#L911)): + +| Response | Key Fields | Description | Source | +|----------|-----------|-------------|--------| +| `groupCreated` | `user, groupInfo` | New group created | [L909](../Shared/Model/AppAPITypes.swift#L913) | +| `sentGroupInvitation` | `user, groupInfo, contact, member` | Group invitation sent | [L910](../Shared/Model/AppAPITypes.swift#L914) | +| `groupMembers` | `user, group: Group` | Group member list | [L914](../Shared/Model/AppAPITypes.swift#L918) | +| `membersRoleUser` | `user, groupInfo, members, toRole` | Role changed | [L918](../Shared/Model/AppAPITypes.swift#L922) | +| `groupUpdated` | `user, toGroup: GroupInfo` | Group profile updated | [L920](../Shared/Model/AppAPITypes.swift#L924) | +| `groupLinkCreated` | `user, groupInfo, groupLink` | Group link created | [L921](../Shared/Model/AppAPITypes.swift#L925) | +| `rcvFileAccepted` | `user, chatItem` | File download started | [L928](../Shared/Model/AppAPITypes.swift#L932) | +| `callInvitations` | `callInvitations: [RcvCallInvitation]` | Pending calls | [L937](../Shared/Model/AppAPITypes.swift#L941) | +| `ntfToken` | `token, status, ntfMode, ntfServer` | Notification token info | [L940](../Shared/Model/AppAPITypes.swift#L944) | +| `versionInfo` | `versionInfo, chatMigrations, agentMigrations` | Core version | [L948](../Shared/Model/AppAPITypes.swift#L952) | +| `cmdOk` | `user_` | Generic success | [L949](../Shared/Model/AppAPITypes.swift#L953) | +| `archiveExported` | `archiveErrors: [ArchiveError]` | Export result | [L953](../Shared/Model/AppAPITypes.swift#L957) | +| `archiveImported` | `archiveErrors: [ArchiveError]` | Import result | [L954](../Shared/Model/AppAPITypes.swift#L958) | +| `appSettings` | `appSettings: AppSettings` | Retrieved settings | [L955](../Shared/Model/AppAPITypes.swift#L959) | + +--- + +## 4. Event Types + +The `ChatEvent` enum ([`AppAPITypes.swift` L1050](../Shared/Model/AppAPITypes.swift#L1055)) represents async events from the Haskell core. These arrive via `chat_recv_msg_wait` polling, not as responses to commands. + +Event processing entry point: [`processReceivedMsg`](../Shared/Model/SimpleXAPI.swift#L2266) in `SimpleXAPI.swift`. + +### Connection Events + +| Event | Key Fields | Description | Source | +|-------|-----------|-------------|--------| +| `contactConnected` | `user, contact, userCustomProfile` | Contact connection established | [L1057](../Shared/Model/AppAPITypes.swift#L1062) | +| `contactConnecting` | `user, contact` | Contact connecting in progress | [L1058](../Shared/Model/AppAPITypes.swift#L1063) | +| `contactSndReady` | `user, contact` | Ready to send to contact | [L1059](../Shared/Model/AppAPITypes.swift#L1064) | +| `contactDeletedByContact` | `user, contact` | Contact deleted by other party | [L1056](../Shared/Model/AppAPITypes.swift#L1061) | +| `contactUpdated` | `user, toContact` | Contact profile updated | [L1061](../Shared/Model/AppAPITypes.swift#L1066) | +| `receivedContactRequest` | `user, contactRequest, chat_` | Incoming contact request | [L1060](../Shared/Model/AppAPITypes.swift#L1065) | +| `subscriptionStatus` | `subscriptionStatus, connections` | Connection subscription change | [L1063](../Shared/Model/AppAPITypes.swift#L1068) | + +### Message Events + +| Event | Key Fields | Description | Source | +|-------|-----------|-------------|--------| +| `newChatItems` | `user, chatItems: [AChatItem]` | New messages received | [L1065](../Shared/Model/AppAPITypes.swift#L1070) | +| `chatItemUpdated` | `user, chatItem: AChatItem` | Message edited remotely | [L1067](../Shared/Model/AppAPITypes.swift#L1072) | +| `chatItemReaction` | `user, added, reaction: ACIReaction` | Reaction added/removed | [L1068](../Shared/Model/AppAPITypes.swift#L1073) | +| `chatItemsDeleted` | `user, chatItemDeletions, byUser` | Messages deleted | [L1069](../Shared/Model/AppAPITypes.swift#L1074) | +| `chatItemsStatusesUpdated` | `user, chatItems: [AChatItem]` | Delivery status changed | [L1066](../Shared/Model/AppAPITypes.swift#L1071) | +| `groupChatItemsDeleted` | `user, groupInfo, chatItemIDs, byUser, member_` | Group items deleted | [L1071](../Shared/Model/AppAPITypes.swift#L1076) | +| `chatInfoUpdated` | `user, chatInfo` | Chat metadata changed | [L1064](../Shared/Model/AppAPITypes.swift#L1069) | + +### Group Events + +| Event | Key Fields | Description | Source | +|-------|-----------|-------------|--------| +| `receivedGroupInvitation` | `user, groupInfo, contact, memberRole` | Group invitation received | [L1072](../Shared/Model/AppAPITypes.swift#L1077) | +| `userAcceptedGroupSent` | `user, groupInfo, hostContact` | Joined group | [L1073](../Shared/Model/AppAPITypes.swift#L1078) | +| `groupLinkConnecting` | `user, groupInfo, hostMember` | Connecting via group link | [L1074](../Shared/Model/AppAPITypes.swift#L1079) | +| `joinedGroupMemberConnecting` | `user, groupInfo, hostMember, member` | Member joining | [L1076](../Shared/Model/AppAPITypes.swift#L1081) | +| `memberRole` | `user, groupInfo, byMember, member, fromRole, toRole` | Role changed | [L1078](../Shared/Model/AppAPITypes.swift#L1083) | +| `memberBlockedForAll` | `user, groupInfo, byMember, member, blocked` | Member blocked | [L1079](../Shared/Model/AppAPITypes.swift#L1084) | +| `deletedMemberUser` | `user, groupInfo, member, withMessages` | Current user removed | [L1080](../Shared/Model/AppAPITypes.swift#L1085) | +| `deletedMember` | `user, groupInfo, byMember, deletedMember` | Member removed | [L1081](../Shared/Model/AppAPITypes.swift#L1086) | +| `leftMember` | `user, groupInfo, member` | Member left | [L1082](../Shared/Model/AppAPITypes.swift#L1087) | +| `groupDeleted` | `user, groupInfo, member` | Group deleted | [L1083](../Shared/Model/AppAPITypes.swift#L1088) | +| `userJoinedGroup` | `user, groupInfo` | Successfully joined | [L1084](../Shared/Model/AppAPITypes.swift#L1089) | +| `joinedGroupMember` | `user, groupInfo, member` | New member joined | [L1085](../Shared/Model/AppAPITypes.swift#L1090) | +| `connectedToGroupMember` | `user, groupInfo, member, memberContact` | E2E session established with member | [L1086](../Shared/Model/AppAPITypes.swift#L1091) | +| `groupUpdated` | `user, toGroup: GroupInfo` | Group profile changed | [L1087](../Shared/Model/AppAPITypes.swift#L1092) | +| `groupMemberUpdated` | `user, groupInfo, fromMember, toMember` | Member info updated | [L1062](../Shared/Model/AppAPITypes.swift#L1067) | + +### File Transfer Events + +| Event | Key Fields | Description | Source | +|-------|-----------|-------------|--------| +| `rcvFileStart` | `user, chatItem` | Download started | [L1092](../Shared/Model/AppAPITypes.swift#L1097) | +| `rcvFileProgressXFTP` | `user, chatItem_, receivedSize, totalSize` | Download progress | [L1093](../Shared/Model/AppAPITypes.swift#L1098) | +| `rcvFileComplete` | `user, chatItem` | Download complete | [L1094](../Shared/Model/AppAPITypes.swift#L1099) | +| `rcvFileSndCancelled` | `user, chatItem, rcvFileTransfer` | Sender cancelled | [L1096](../Shared/Model/AppAPITypes.swift#L1101) | +| `rcvFileError` | `user, chatItem_, agentError, rcvFileTransfer` | Download error | [L1097](../Shared/Model/AppAPITypes.swift#L1102) | +| `sndFileStart` | `user, chatItem, sndFileTransfer` | Upload started | [L1100](../Shared/Model/AppAPITypes.swift#L1105) | +| `sndFileComplete` | `user, chatItem, sndFileTransfer` | Upload complete (inline) | [L1101](../Shared/Model/AppAPITypes.swift#L1106) | +| `sndFileProgressXFTP` | `user, chatItem_, fileTransferMeta, sentSize, totalSize` | Upload progress | [L1103](../Shared/Model/AppAPITypes.swift#L1108) | +| `sndFileCompleteXFTP` | `user, chatItem, fileTransferMeta` | XFTP upload complete | [L1105](../Shared/Model/AppAPITypes.swift#L1110) | +| `sndFileError` | `user, chatItem_, fileTransferMeta, errorMessage` | Upload error | [L1107](../Shared/Model/AppAPITypes.swift#L1112) | + +### Call Events + +| Event | Key Fields | Description | Source | +|-------|-----------|-------------|--------| +| `callInvitation` | `callInvitation: RcvCallInvitation` | Incoming call | [L1110](../Shared/Model/AppAPITypes.swift#L1115) | +| `callOffer` | `user, contact, callType, offer, sharedKey, askConfirmation` | SDP offer received | [L1111](../Shared/Model/AppAPITypes.swift#L1116) | +| `callAnswer` | `user, contact, answer` | SDP answer received | [L1112](../Shared/Model/AppAPITypes.swift#L1117) | +| `callExtraInfo` | `user, contact, extraInfo` | ICE candidates received | [L1113](../Shared/Model/AppAPITypes.swift#L1118) | +| `callEnded` | `user, contact` | Call ended by remote | [L1114](../Shared/Model/AppAPITypes.swift#L1119) | + +### Connection Security Events + +| Event | Key Fields | Description | Source | +|-------|-----------|-------------|--------| +| `contactSwitch` | `user, contact, switchProgress` | Key rotation progress | [L1052](../Shared/Model/AppAPITypes.swift#L1057) | +| `groupMemberSwitch` | `user, groupInfo, member, switchProgress` | Member key rotation | [L1053](../Shared/Model/AppAPITypes.swift#L1058) | +| `contactRatchetSync` | `user, contact, ratchetSyncProgress` | Ratchet sync progress | [L1054](../Shared/Model/AppAPITypes.swift#L1059) | +| `groupMemberRatchetSync` | `user, groupInfo, member, ratchetSyncProgress` | Member ratchet sync | [L1055](../Shared/Model/AppAPITypes.swift#L1060) | + +### System Events + +| Event | Key Fields | Description | Source | +|-------|-----------|-------------|--------| +| `chatSuspended` | -- | Core suspended | [L1051](../Shared/Model/AppAPITypes.swift#L1056) | + +--- + +## 5. Error Types + +Defined in [`SimpleXChat/APITypes.swift` L695](../SimpleXChat/APITypes.swift#L699): + +```swift +public enum ChatError: Decodable, Hashable { + case error(errorType: ChatErrorType) + case errorAgent(agentError: AgentErrorType) + case errorStore(storeError: StoreError) + case errorDatabase(databaseError: DatabaseError) + case errorRemoteCtrl(remoteCtrlError: RemoteCtrlError) + case invalidJSON(json: String) + case unexpectedResult(type: String) +} +``` + +### Error Categories + +| Category | Enum | Description | Source | +|----------|------|-------------|--------| +| Chat logic | `ChatErrorType` | Business logic errors (e.g., invalid state, permission denied) | [`APITypes.swift` L717](../SimpleXChat/APITypes.swift#L722) | +| SMP Agent | `AgentErrorType` | Protocol/network errors from the SMP agent layer | [`APITypes.swift` L873](../SimpleXChat/APITypes.swift#L878) | +| Database store | `StoreError` | SQLite query/constraint errors | [`APITypes.swift` L796](../SimpleXChat/APITypes.swift#L801) | +| Database engine | `DatabaseError` | DB open/migration/encryption errors | [`APITypes.swift` L860](../SimpleXChat/APITypes.swift#L865) | +| Remote control | `RemoteCtrlError` | Remote desktop session errors | [`APITypes.swift` L1043](../SimpleXChat/APITypes.swift#L1048) | +| Parse failure | `invalidJSON` | Failed to decode response JSON | [`APITypes.swift` L695](../SimpleXChat/APITypes.swift#L699) | +| Unexpected | `unexpectedResult` | Response type does not match expected | [`APITypes.swift` L695](../SimpleXChat/APITypes.swift#L699) | + +--- + +## 6. FFI Bridge Functions + +Defined in [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift): + +### Synchronous (blocking current thread) + +```swift +// Throws on error, returns typed result +func chatSendCmdSync( // SimpleXAPI.swift L91 + _ cmd: ChatCommand, + bgTask: Bool = true, + bgDelay: Double? = nil, + ctrl: chat_ctrl? = nil, + log: Bool = true +) throws -> R + +// Returns APIResult (caller handles error) +func chatApiSendCmdSync( // SimpleXAPI.swift L96 + _ cmd: ChatCommand, + bgTask: Bool = true, + bgDelay: Double? = nil, + ctrl: chat_ctrl? = nil, + retryNum: Int32 = 0, + log: Bool = true +) -> APIResult +``` + +### Asynchronous (Swift concurrency) + +```swift +// Throws on error, returns typed result +func chatSendCmd( // SimpleXAPI.swift L117 + _ cmd: ChatCommand, + bgTask: Bool = true, + bgDelay: Double? = nil, + ctrl: chat_ctrl? = nil, + log: Bool = true +) async throws -> R + +// Returns APIResult with optional retry on network errors +func chatApiSendCmdWithRetry( // SimpleXAPI.swift L122 + _ cmd: ChatCommand, + bgTask: Bool = true, + bgDelay: Double? = nil, + inProgress: BoxedValue? = nil, + retryNum: Int32 = 0 +) async -> APIResult? +``` + +### Low-Level FFI + +```swift +// Direct C FFI call -- serializes cmd.cmdString, calls chat_send_cmd_retry, decodes response +public func sendSimpleXCmd( // API.swift L115 + _ cmd: ChatCmdProtocol, + _ ctrl: chat_ctrl?, + retryNum: Int32 = 0 +) -> APIResult +``` + +### Event Receiver + +```swift +// Polls for async events from the Haskell core +func chatRecvMsg( // SimpleXAPI.swift L230 + _ ctrl: chat_ctrl? = nil +) async -> APIResult? + +// Processes a received event and updates app state +func processReceivedMsg( // SimpleXAPI.swift L2248 + _ res: ChatEvent +) async +``` + +--- + +## 7. Result Type + +Defined in [`SimpleXChat/APITypes.swift` L26](../SimpleXChat/APITypes.swift#L27): + +```swift +public enum APIResult: Decodable where R: Decodable, R: ChatAPIResult { + case result(R) // Successful response + case error(ChatError) // Error response from core + case invalid(type: String, json: Data) // Undecodable response + + public var responseType: String { ... } + public var unexpected: ChatError { ... } +} + +public protocol ChatAPIResult: Decodable { // APITypes.swift L63 + var responseType: String { get } + var details: String { get } + static func fallbackResult(_ type: String, _ json: NSDictionary) -> Self? +} +``` + +The `decodeAPIResult` function ([`APITypes.swift` L83](../SimpleXChat/APITypes.swift#L86)) handles JSON decoding with fallback logic: +1. Try standard `JSONDecoder.decode(APIResult.self, from: data)` +2. If that fails, try manual JSON parsing via `JSONSerialization` +3. Check for `"error"` key -- return `.error` +4. Check for `"result"` key -- try `R.fallbackResult` or return `.invalid` +5. Last resort: return `.invalid(type: "invalid", json: ...)` + +--- + +## Source Files + +| File | Path | +|------|------| +| ChatCommand enum | [`Shared/Model/AppAPITypes.swift` L14](../Shared/Model/AppAPITypes.swift#L15) | +| ChatResponse0/1/2 enums | [`Shared/Model/AppAPITypes.swift` L647, L768, L907](../Shared/Model/AppAPITypes.swift#L649) | +| ChatEvent enum | [`Shared/Model/AppAPITypes.swift` L1050](../Shared/Model/AppAPITypes.swift#L1055) | +| APIResult, ChatError | [`SimpleXChat/APITypes.swift` L26, L695](../SimpleXChat/APITypes.swift#L27) | +| FFI bridge functions | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift) | +| Low-level FFI | [`SimpleXChat/API.swift`](../SimpleXChat/API.swift) | +| Data types | `SimpleXChat/ChatTypes.swift` | +| C header | `SimpleXChat/SimpleX.h` | +| Haskell controller | `../../src/Simplex/Chat/Controller.hs` | diff --git a/apps/ios/spec/architecture.md b/apps/ios/spec/architecture.md new file mode 100644 index 0000000000..84d9d3269d --- /dev/null +++ b/apps/ios/spec/architecture.md @@ -0,0 +1,298 @@ +# SimpleX Chat iOS -- System Architecture + +> Technical specification for the iOS app's layered architecture, FFI bridge, event system, and extension model. +> +> Related specs: [README](README.md) | [API Reference](api.md) | [State Management](state.md) | [Database](database.md) +> Related product: [Product Overview](../product/README.md) + +**Source:** [`SimpleXApp.swift`](../Shared/SimpleXApp.swift#L1-L183) | [`AppDelegate.swift`](../Shared/AppDelegate.swift#L1-L209) | [`ContentView.swift`](../Shared/ContentView.swift#L1-L513) | [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1373) | [`SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L1-L2915) | [`AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L1-L2357) | [`APITypes.swift`](../SimpleXChat/APITypes.swift#L1-L1071) | [`API.swift`](../SimpleXChat/API.swift#L1-L388) + +--- + +## Table of Contents + +1. [Layered Architecture](#1-layered-architecture) +2. [FFI Bridge](#2-ffi-bridge) +3. [Event Streaming](#3-event-streaming) +4. [Database Architecture](#4-database-architecture) +5. [App Lifecycle](#5-app-lifecycle) +6. [Extension Architecture](#6-extension-architecture) +7. [Remote Desktop Control](#7-remote-desktop-control) + +--- + +## [1. Layered Architecture](../Shared/SimpleXApp.swift#L17-L184) + +The app follows a strict layered model where each layer communicates only with its immediate neighbor: + +``` +┌─────────────────────────────────────────┐ +│ SwiftUI Views │ Rendering, user interaction +│ (ChatListView, ChatView, ComposeView) │ +├─────────────────────────────────────────┤ +│ ChatModel (ObservableObject) │ App state, @Published properties +│ ItemsModel, Chat, ChatTagsModel │ Per-chat state, tag filtering +├─────────────────────────────────────────┤ +│ SimpleXAPI (FFI Bridge) │ chatSendCmd/chatApiSendCmd +│ AppAPITypes (ChatCommand/Response) │ JSON serialization/deserialization +├─────────────────────────────────────────┤ +│ C FFI Layer │ chat_send_cmd_retry, chat_recv_msg_wait +│ (SimpleX.h, libsimplex.a) │ Compiled Haskell via GHC cross-compiler +├─────────────────────────────────────────┤ +│ Haskell Core (chat_ctrl) │ Chat logic, chat protocol (x-events), +│ (Simplex.Chat.Controller) │ database operations, file management +├─────────────────────────────────────────┤ +│ simplexmq library (external) │ SMP/XFTP protocols, SMP Agent, +│ (github.com/simplex-chat/simplexmq) │ double-ratchet (PQDR), transport (TLS) +└─────────────────────────────────────────┘ +``` + +**Key invariant**: No SwiftUI view directly calls FFI functions. All communication flows through `ChatModel` or dedicated API functions in `SimpleXAPI.swift`. + +### Source Files + +| Layer | File | Role | Line | +|-------|------|------|------| +| Views | [`Shared/Views/ChatList/ChatListView.swift`](../Shared/Views/ChatList/ChatListView.swift) | Chat list rendering | | +| Views | [`Shared/Views/Chat/ChatView.swift`](../Shared/Views/Chat/ChatView.swift) | Conversation rendering | | +| State | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L337) | `ChatModel`, `ItemsModel`, `Chat` classes | L337, L74, L1271 | +| API | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L93) | FFI bridge functions | L93 | +| API | [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L15) | `ChatCommand`, `ChatResponse`, `ChatEvent` enums | L15, L649, L1055 | +| FFI | [`SimpleXChat/SimpleX.h`](../SimpleXChat/SimpleX.h#L1-L49) | C header declaring Haskell exports | | +| FFI | [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift#L27) | `APIResult`, `ChatError`, `ChatCmdProtocol` | L27, L699, L17 | +| Core | `../../src/Simplex/Chat/Controller.hs` | Haskell command processor — see `processCommand` in `Controller.hs` | | + +--- + +## [2. FFI Bridge](../SimpleXChat/SimpleX.h#L1-L49) + +### [C Functions (SimpleX.h)](../SimpleXChat/SimpleX.h#L1-L49) + +The Haskell core exposes these C functions, declared in `SimpleXChat/SimpleX.h`: + +```c +typedef void* chat_ctrl; + +// Initialize database, apply migrations, return controller +char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm, + int backgroundMode, chat_ctrl *ctrl); + +// Send command string, return JSON response string +char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum); + +// Block until next async event arrives (or timeout) +char *chat_recv_msg_wait(chat_ctrl ctl, int wait); + +// Close/reopen database store +char *chat_close_store(chat_ctrl ctl); +char *chat_reopen_store(chat_ctrl ctl); + +// Utility: markdown parsing, server validation, password hashing +char *chat_parse_markdown(char *str); +char *chat_parse_server(char *str); +char *chat_password_hash(char *pwd, char *salt); + +// File encryption/decryption +char *chat_write_file(chat_ctrl ctl, char *path, char *data, int len); +char *chat_read_file(char *path, char *key, char *nonce); +char *chat_encrypt_file(chat_ctrl ctl, char *fromPath, char *toPath); +char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath); +``` + +### [Swift Bridge Functions (SimpleXAPI.swift)](../Shared/Model/SimpleXAPI.swift#L93-L221) + +```swift +// Synchronous send -- blocks calling thread +func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, + bgDelay: Double? = nil, ctrl: chat_ctrl? = nil) throws -> R // L91 + +// Async send -- dispatches to background +func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, + bgDelay: Double? = nil, ctrl: chat_ctrl? = nil) async -> APIResult // L215 + +// Low-level FFI call -- serializes command to string, calls chat_send_cmd_retry, decodes JSON +func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl?, + retryNum: Int32 = 0) -> APIResult // SimpleXChat/API.swift L114 +``` + +### Data Flow + +1. Swift constructs a `ChatCommand` enum value (e.g., `.apiSendMessages(type:id:scope:live:ttl:composedMessages:)`) +2. [`ChatCommand.cmdString`](../Shared/Model/AppAPITypes.swift#L15) serializes it to a command string (e.g., `"/_send @1 json {...}"`) +3. [`sendSimpleXCmd`](../SimpleXChat/API.swift#L115) passes the string to `chat_send_cmd_retry` via C FFI +4. Haskell core processes the command, returns JSON response string +5. Swift decodes JSON into [`APIResult`](../SimpleXChat/APITypes.swift#L27) where `R: ChatAPIResult` +6. Result is either `.result(R)`, `.error(ChatError)`, or `.invalid(type, json)` + +### [Background Task Protection](../Shared/Model/SimpleXAPI.swift#L54-L79) + +All FFI calls are wrapped in [`beginBGTask()`](../Shared/Model/SimpleXAPI.swift#L54) / `endBackgroundTask()` to prevent iOS from killing the app mid-operation. The `maxTaskDuration` is 15 seconds. + +--- + +## [3. Event Streaming](../Shared/Model/SimpleXAPI.swift#L2220-L2916) + +The Haskell core emits async events (new messages, connection status changes, file progress, etc.) that are not direct responses to commands. These are received via polling: + +``` +Haskell Core --[chat_recv_msg_wait]--> Swift event loop --> ChatModel update --> SwiftUI re-render +``` + +The event loop is implemented in [`ChatReceiver`](../Shared/Model/SimpleXAPI.swift#L2220-L2263), and events are dispatched by [`processReceivedMsg`](../Shared/Model/SimpleXAPI.swift#L2266). + +### [Event Types (ChatEvent enum)](../Shared/Model/AppAPITypes.swift#L1055-L1129) + +Key async events delivered from core to UI: + +| Event | Description | Line | +|-------|-------------|------| +| `newChatItems` | New messages received | [L1070](../Shared/Model/AppAPITypes.swift#L1070) | +| `chatItemUpdated` | Message edited by sender | [L1072](../Shared/Model/AppAPITypes.swift#L1072) | +| `chatItemsDeleted` | Messages deleted | [L1074](../Shared/Model/AppAPITypes.swift#L1074) | +| `chatItemReaction` | Reaction added/removed | [L1073](../Shared/Model/AppAPITypes.swift#L1073) | +| `contactConnected` | New contact connected | [L1062](../Shared/Model/AppAPITypes.swift#L1062) | +| `contactUpdated` | Contact profile changed | [L1066](../Shared/Model/AppAPITypes.swift#L1066) | +| `receivedGroupInvitation` | Group invitation received | [L1077](../Shared/Model/AppAPITypes.swift#L1077) | +| `groupMemberUpdated` | Group member info changed | [L1067](../Shared/Model/AppAPITypes.swift#L1067) | +| `callInvitation` | Incoming call | [L1115](../Shared/Model/AppAPITypes.swift#L1115) | +| `chatSuspended` | Core suspended (background) | [L1056](../Shared/Model/AppAPITypes.swift#L1056) | +| `rcvFileComplete` | File download finished | [L1099](../Shared/Model/AppAPITypes.swift#L1099) | +| `sndFileCompleteXFTP` | File upload finished | [L1110](../Shared/Model/AppAPITypes.swift#L1110) | + +Events are decoded as [`ChatEvent`](../Shared/Model/AppAPITypes.swift#L1055) enum in `Shared/Model/AppAPITypes.swift` and dispatched to update `ChatModel` / `ItemsModel` properties, triggering SwiftUI view re-renders via `@Published` property observation. + +--- + +## [4. Database Architecture](../SimpleXChat/FileUtils.swift#L70-L294) + +Two SQLite databases in the app group container (shared with NSE): + +| Database | File | Contents | +|----------|------|----------| +| Chat DB | `simplex_v1_chat.db` | Messages, contacts, groups, profiles, files, tags, preferences | +| Agent DB | `simplex_v1_agent.db` | SMP connections, keys, queues, server info | + +Both databases use the `DB_FILE_PREFIX = "simplex_v1"` prefix. The database path is resolved via [`getAppDatabasePath()`](../SimpleXChat/FileUtils.swift#L70) in `SimpleXChat/FileUtils.swift`, which checks `dbContainerGroupDefault` to determine whether to use the app group container or legacy documents directory. + +See [Database & Storage specification](database.md) for full details. + +--- + +## [5. App Lifecycle](../Shared/SimpleXApp.swift#L17-L184) + +### [Initialization Sequence (SimpleXApp.swift)](../Shared/SimpleXApp.swift#L17-L38) + +```swift +// SimpleXApp.init() +1. haskell_init() // Initialize Haskell RTS (background queue, sync) +2. UserDefaults.register(defaults:) // Register app preference defaults +3. setGroupDefaults() // Sync preferences to app group container +4. setDbContainer() // Set database path L122 +5. BGManager.shared.register() // Register background task handlers +6. NtfManager.shared.registerCategories() // Register notification action categories +``` + +### State Transitions + +``` + ┌──────────┐ + │ Launched │ + └─────┬─────┘ + │ initChatAndMigrate() + v + ┌──────────┐ + │ DB Setup │ chat_migrate_init_key() + └─────┬─────┘ + │ startChat() SimpleXAPI.swift L2098 + v + ┌──────────┐ + │ Active │ apiActivateChat() SimpleXAPI.swift L358 + └─────┬─────┘ + │ scenePhase == .background + v + ┌──────────┐ + │Background │ apiSuspendChat(timeoutMicroseconds:) SimpleXAPI.swift L368 + └─────┬─────┘ + │ scenePhase == .active + v + ┌──────────┐ + │ Active │ startChatAndActivate() + └──────────┘ +``` + +### [Scene Phase Handling (SimpleXApp.swift)](../Shared/SimpleXApp.swift#L38-L123) + +- **`.active`**: Calls `startChatAndActivate()`, processes pending notification responses, refreshes chat list and call invitations +- **`.background`**: Records authentication timestamp, calls `suspendChat()` (unless CallKit call active), schedules `BGManager` background refresh, updates badge count +- **`.inactive`**: No explicit handling (transitional state) + +### CallKit Exception + +When a CallKit call is active during backgrounding, chat suspension is deferred (`CallController.shared.shouldSuspendChat = true`) until the call ends, to maintain the WebRTC session. + +--- + +## [6. Extension Architecture](../SimpleX%20NSE/NotificationService.swift#L1-L1228) + +### [Notification Service Extension (NSE)](../SimpleX%20NSE/NotificationService.swift#L1-L1228) + +The NSE ([`SimpleX NSE/NotificationService.swift`](../SimpleX%20NSE/NotificationService.swift#L1-L1228)) is a separate process that: + +1. Receives encrypted push notification payload from APNs +2. Initializes its own Haskell core instance (`chat_ctrl`) with shared database access +3. Decrypts the push payload using stored keys +4. Generates a visible `UNMutableNotificationContent` with the decrypted message preview +5. Delivers the notification to the user + +**Database sharing**: Both main app and NSE access the same database files in the app group container (`APP_GROUP_NAME`). Coordination uses file locks to prevent concurrent write conflicts. + +**Lifecycle**: The NSE has a ~30-second execution window per notification. It must initialize Haskell RTS, open the database, decrypt, and deliver within this window. + +### Share Extension (SE) + +The Share Extension (`SimpleX SE/`) allows sharing content (text, images, files) from other apps into SimpleX conversations. + +--- + +## [7. Remote Desktop Control](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545) + +Optional desktop pairing allows controlling the mobile app from a desktop client: + +- **Pairing**: Encrypted QR code scanned by desktop client establishes a session +- **Commands**: [`connectRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1613), [`findKnownRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1620), [`confirmRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1624), [`verifyRemoteCtrlSession`](../Shared/Model/SimpleXAPI.swift#L1630), [`listRemoteCtrls`](../Shared/Model/SimpleXAPI.swift#L1636), [`stopRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1642), [`deleteRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1646) +- **State**: [`ChatModel.remoteCtrlSession`](../Shared/Model/ChatModel.swift#L395)`: RemoteCtrlSession?` tracks the active session +- **Transport**: Encrypted reverse HTTP transport between mobile and desktop +- **Source**: [`Shared/Views/RemoteAccess/ConnectDesktopView.swift`](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545), see `Remote.hs` in `../../src/Simplex/Chat/` + +--- + +## Source Files + +| File | Path | Line | +|------|------|------| +| App entry point | [`Shared/SimpleXApp.swift`](../Shared/SimpleXApp.swift#L17) | L17 | +| App delegate | [`Shared/AppDelegate.swift`](../Shared/AppDelegate.swift#L15) | L15 | +| Root view | [`Shared/ContentView.swift`](../Shared/ContentView.swift#L24) | L24 | +| FFI bridge | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L93) | L93 | +| Low-level FFI | [`SimpleXChat/API.swift`](../SimpleXChat/API.swift#L115) | L115 | +| App state | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L337) | L337 | +| API types | [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L15) | L15 | +| Shared types | [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift#L27) | L27 | +| C header | [`SimpleXChat/SimpleX.h`](../SimpleXChat/SimpleX.h#L1-L49) | | +| NSE | [`SimpleX NSE/NotificationService.swift`](../SimpleX%20NSE/NotificationService.swift#L1-L1228) | | +| Haskell core | `../../src/Simplex/Chat/Controller.hs` — see `processCommand` in `Controller.hs` | | +| Chat protocol (x-events, message envelopes) | `../../src/Simplex/Chat/Protocol.hs` | | + +### External: simplexmq Library + +The lower-level protocol and encryption layers are in the separate [simplexmq](https://github.com/simplex-chat/simplexmq) library: + +| Component | Spec | Implementation | +|-----------|------|----------------| +| SMP protocol | `simplexmq/protocol/simplex-messaging.md` | `simplexmq/src/Simplex/Messaging/Protocol.hs` | +| XFTP protocol | `simplexmq/protocol/xftp.md` | `simplexmq/src/Simplex/FileTransfer/Protocol.hs` | +| SMP Agent (duplex connections) | `simplexmq/protocol/agent-protocol.md` | `simplexmq/src/Simplex/Messaging/Agent.hs` | +| Double ratchet (PQDR) | `simplexmq/protocol/pqdr.md` | `simplexmq/src/Simplex/Messaging/Crypto/Ratchet.hs` | +| Post-quantum KEM (sntrup761) | `simplexmq/protocol/pqdr.md` | `simplexmq/src/Simplex/Messaging/Crypto/SNTRUP761.hs` | +| TLS transport | — | `simplexmq/src/Simplex/Messaging/Transport.hs` | +| File encryption | — | `simplexmq/src/Simplex/Messaging/Crypto/File.hs` | diff --git a/apps/ios/spec/client/chat-list.md b/apps/ios/spec/client/chat-list.md new file mode 100644 index 0000000000..0eb3cd75f7 --- /dev/null +++ b/apps/ios/spec/client/chat-list.md @@ -0,0 +1,280 @@ +# SimpleX Chat iOS -- Chat List Module + +> Technical specification for the conversation list, filtering, search, swipe actions, and user picker. +> +> Related specs: [Chat View](chat-view.md) | [Navigation](navigation.md) | [State Management](../state.md) | [README](../README.md) +> Related product: [Chat List View](../../product/views/chat-list.md) + +**Source:** [`ChatListView.swift`](../../Shared/Views/ChatList/ChatListView.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatListView](#2-chatlistview) +3. [ChatPreviewView](#3-chatpreviewview) +4. [ChatListNavLink](#4-chatlistnavlink) +5. [Filtering & Tags](#5-filtering--tags) +6. [Search](#6-search) +7. [Swipe Actions](#7-swipe-actions) +8. [UserPicker](#8-userpicker) +9. [Floating Action Button](#9-floating-action-button) + +--- + +## 1. Overview + +The chat list is the main screen of the app, displaying all conversations for the current user. It provides: + +- Conversation previews with unread badges +- Filter tabs (All, Unread, Favorites, Groups, Contacts, Business, user-defined tags) +- Search across chat names and message content +- Swipe actions for quick operations +- User profile switcher +- Floating action button for new conversations + +``` +ChatListView +├── Navigation Bar +│ ├── User avatar (tap → UserPicker) +│ └── Filter tabs (TagListView) +├── Search bar (on pull-down or tap) +├── Chat List (List/LazyVStack) +│ └── ChatListNavLink (per conversation) +│ └── ChatPreviewView +│ ├── Avatar +│ ├── Chat name + last message preview +│ ├── Timestamp +│ └── Unread badge +├── FAB (New Chat button) +└── Pending connection cards +``` + +--- + +## 2. [`ChatListView`](../../Shared/Views/ChatList/ChatListView.swift#L142) {#2-chatlistview} + +**File**: `Shared/Views/ChatList/ChatListView.swift` + +The root list view. Key responsibilities: + +### Data Source +- Reads `ChatModel.shared.chats` (all conversations) +- Applies active filter from `ChatTagsModel.shared.activeFilter` +- Applies search query filtering via [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480) +- Sorts by last activity (most recent first), with pinned chats at top + +### Layout +- Uses SwiftUI `List` with `ForEach` over filtered chats +- Each row is a `ChatListNavLink` wrapping a `ChatPreviewView` +- Pull-to-refresh triggers `updateChats()` API call +- Empty state: `ChatHelp` view with getting-started guidance + +### Connection Cards +- Pending contact connections (`ChatInfo.contactConnection`) shown as cards +- Contact requests (`ChatInfo.contactRequest`) shown with accept/reject UI via `ContactRequestView` + +### Key Functions + +| Function | Line | Description | +|----------|------|-------------| +| [`body`](../../Shared/Views/ChatList/ChatListView.swift#L168) | 163 | Main view body | +| [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480) | 472 | Applies active filter and search to chat list | +| [`searchString()`](../../Shared/Views/ChatList/ChatListView.swift#L523) | 514 | Normalizes search text for comparison | +| [`unreadBadge()`](../../Shared/Views/ChatList/ChatListView.swift#L454) | 448 | Renders unread count circle badge | +| [`stopAudioPlayer()`](../../Shared/Views/ChatList/ChatListView.swift#L474) | 467 | Stops any playing voice message | + +--- + +## 3. [`ChatPreviewView`](../../Shared/Views/ChatList/ChatPreviewView.swift#L13) {#3-chatpreviewview} + +**File**: `Shared/Views/ChatList/ChatPreviewView.swift` + +Renders a single row in the chat list. Shows: + +| Element | Source | Description | +|---------|--------|-------------| +| Avatar | `chatInfo.image` | Profile image or default icon | +| Chat name | `chatInfo.displayName` | Contact name, group name, or connection label | +| Last message | `chat.chatItems.last` | Preview text of most recent message | +| Timestamp | `chat.chatItems.last?.timestampText` | Relative time of last message | +| Unread badge | `chat.chatStats.unreadCount` | Circular badge with unread count | +| Mute icon | `chatInfo.chatSettings?.enableNtfs` | Bell-slash icon if notifications muted | +| Pin icon | -- | Pin indicator for pinned chats | +| Incognito icon | Contact.contactConnIncognito | Incognito mode indicator | +| Delivery status | Last sent item's `meta.itemStatus` | Check marks for delivery confirmation | + +### Preview Text Rendering +- Text messages: first line of message content +- Images: camera icon + caption (if any) +- Files: paperclip icon + filename +- Voice: microphone icon + duration +- Calls: phone icon + call status +- Group events: system event description +- Encrypted/deleted: placeholder text + +--- + +## 4. [`ChatListNavLink`](../../Shared/Views/ChatList/ChatListNavLink.swift#L44) {#4-chatlistnavlink} + +**File**: `Shared/Views/ChatList/ChatListNavLink.swift` + +Wraps `ChatPreviewView` in a navigation link with tap and swipe behavior: + +### Tap Behavior +- Direct chat: navigates to `ChatView` via `ItemsModel.loadOpenChat(chatId)` -- [`contactNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L95) L93 +- Group chat: navigates to `ChatView` -- [`groupNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L217) L214 +- Contact request: shows `ContactRequestView` with accept/reject -- [`contactRequestNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L495) L486 +- Contact connection: shows `ContactConnectionInfo` -- [`contactConnectionNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L530) L520 +- Notes folder: navigates to `ChatView` -- [`noteFolderNavLink()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L302) L298 + +### Navigation +- Uses `NavigationLink` (iOS 15) or programmatic navigation (iOS 16+) +- Sets `ChatModel.chatId` to trigger navigation +- `ItemsModel.loadOpenChat()` loads messages with a 250ms navigation delay for smooth animation + +--- + +## 5. Filtering & Tags + +### Filter Tabs ([`TagListView`](../../Shared/Views/ChatList/TagListView.swift#L20)) + +**File**: `Shared/Views/ChatList/TagListView.swift` + +Horizontal scrolling tab bar below the navigation bar. Tabs: + +| Tab | Filter | Shows | +|-----|--------|-------| +| All | `nil` | All conversations | +| Unread | `.unread` | Conversations with unread messages | +| Favorites | `.presetTag(.favorites)` | Favorited conversations | +| Groups | `.presetTag(.groups)` | Group conversations | +| Contacts | `.presetTag(.contacts)` | Direct conversations | +| Business | `.presetTag(.business)` | Business conversations | +| Group Reports | `.presetTag(.groupReports)` | Groups with pending reports | +| User tags | `.userTag(ChatTag)` | User-defined custom tags | + +Filter matching is handled by [`presetTagMatchesChat()`](../../Shared/Views/ChatList/ChatListView.swift#L910) (L910) and the in-view [`TagsView`](../../Shared/Views/ChatList/ChatListView.swift#L705) struct (L705). + +### ChatTagsModel State + +Filtering state is managed by [`ChatTagsModel`](../../Shared/Model/ChatModel.swift#L189) (`ChatModel.swift` L183): + +```swift +class ChatTagsModel: ObservableObject { + @Published var userTags: [ChatTag] = [] + @Published var activeFilter: ActiveFilter? = nil + @Published var presetTags: [PresetTag: Int] = [:] // count per preset tag + @Published var unreadTags: [Int64: Int] = [:] // unread count per user tag +} +``` + +- `presetTags` counts are updated whenever `chats` changes via [`updateChatTags()`](../../Shared/Model/ChatModel.swift#L197) (L197) +- Tags with zero matching chats are auto-hidden +- Active filter is auto-cleared when its tag has no matching chats + +### Supporting Types + +| Type | File | Line | Description | +|------|------|------|-------------| +| [`PresetTag`](../../Shared/Views/ChatList/ChatListView.swift#L36) | ChatListView.swift | 34 | Enum of built-in filter categories | +| [`ActiveFilter`](../../Shared/Views/ChatList/ChatListView.swift#L52) | ChatListView.swift | 49 | Enum wrapping preset, user-tag, or unread filter | +| [`setActiveFilter()`](../../Shared/Views/ChatList/ChatListView.swift#L889) | ChatListView.swift | 878 | Applies a filter and persists selection | + +### Tag Management Commands +- `apiCreateChatTag(tag: ChatTagData)` -- create tag +- `apiSetChatTags(type:, id:, tagIds:)` -- assign tags to a chat +- `apiDeleteChatTag(tagId:)` -- delete tag +- `apiUpdateChatTag(tagId:, tagData:)` -- rename tag +- `apiReorderChatTags(tagIds:)` -- reorder tags + +--- + +## 6. Search + +Search is available via pull-down gesture or search button in the navigation bar. + +**Search bar UI:** [`ChatListSearchBar`](../../Shared/Views/ChatList/ChatListView.swift#L587) (ChatListView.swift L578) + +### Filtering Logic +- Filters `ChatModel.chats` by matching search text against: + - `chatInfo.displayName` (contact/group name) + - `chatInfo.localAlias` (local alias) + - `chatInfo.fullName` (full name) +- For deeper message content search, uses `apiGetChat(chatId:, search:)` parameter +- Core logic in [`filteredChats()`](../../Shared/Views/ChatList/ChatListView.swift#L480) (L480) and [`searchString()`](../../Shared/Views/ChatList/ChatListView.swift#L523) (L523) + +### Search Results +- Matching chats are displayed in the same list format +- Results update as the user types (debounced) +- Clearing search restores the full filtered list + +--- + +## 7. Swipe Actions + +`ChatListNavLink` provides swipe actions on each row: + +### Leading Swipe (left-to-right) + +| Action | Icon | Handler | Line | API | Condition | +|--------|------|---------|------|-----|-----------| +| Pin / Unpin | pin | [`toggleFavoriteButton()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L353) | 347 | `apiSetChatSettings` (favorite) | Always | +| Read / Unread | envelope | [`markReadButton()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L333) | 328 | `apiChatRead` / `apiChatUnread` | Always | + +### Trailing Swipe (right-to-left) + +| Action | Icon | Handler | Line | API | Condition | +|--------|------|---------|------|-----|-----------| +| Mute / Unmute | bell.slash | [`toggleNtfsButton()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L372) | 365 | `apiSetChatSettings` (enableNtfs) | Always | +| Clear | trash | [`clearChatButton()`](../../Shared/Views/ChatList/ChatListNavLink.swift#L393) | 385 | `apiClearChat` | Has messages | +| Delete | trash.fill | -- | -- | `apiDeleteChat` | Not active chat | +| Tag | tag | -- | -- | `apiSetChatTags` | Always | + +--- + +## 8. [`UserPicker`](../../Shared/Views/ChatList/UserPicker.swift#L10) {#8-userpicker} + +**File**: `Shared/Views/ChatList/UserPicker.swift` + +Triggered by tapping the user avatar in the navigation bar. Presented as a sheet with: + +| Section | Contents | +|---------|----------| +| User list | All non-hidden users with unread counts | +| Active user | Highlighted with checkmark | +| Actions | Settings, Your SimpleX address, User profiles | + +### User Switching +- Tapping a different user calls `apiSetActiveUser(userId:)` +- Triggers `apiGetChats` for the new user +- `ChatModel.currentUser` updates, causing full UI refresh +- Hidden users are not shown (require password entry via settings) + +--- + +## 9. Floating Action Button + +The FAB (floating action button) in the bottom-right corner opens the new chat flow: + +- Tap: opens `NewChatView` sheet for creating a new contact connection or group +- Shows options: Create link, Scan QR code, Paste link, Create group + +--- + +## Source Files + +| File | Path | Key struct | Line | +|------|------|------------|------| +| Chat list view | [`ChatListView.swift`](../../Shared/Views/ChatList/ChatListView.swift) | `ChatListView` | [138](../../Shared/Views/ChatList/ChatListView.swift#L142) | +| Chat preview row | [`ChatPreviewView.swift`](../../Shared/Views/ChatList/ChatPreviewView.swift) | `ChatPreviewView` | [12](../../Shared/Views/ChatList/ChatPreviewView.swift#L13) | +| Navigation link wrapper | [`ChatListNavLink.swift`](../../Shared/Views/ChatList/ChatListNavLink.swift) | `ChatListNavLink` | [43](../../Shared/Views/ChatList/ChatListNavLink.swift#L44) | +| Tag filter tabs | [`TagListView.swift`](../../Shared/Views/ChatList/TagListView.swift) | `TagListView` | [19](../../Shared/Views/ChatList/TagListView.swift#L20) | +| User picker sheet | [`UserPicker.swift`](../../Shared/Views/ChatList/UserPicker.swift) | `UserPicker` | [9](../../Shared/Views/ChatList/UserPicker.swift#L10) | +| Getting started help | [`ChatHelp.swift`](../../Shared/Views/ChatList/ChatHelp.swift) | | | +| Contact request view | [`ContactRequestView.swift`](../../Shared/Views/ChatList/ContactRequestView.swift) | | | +| Contact connection info | [`ContactConnectionInfo.swift`](../../Shared/Views/ChatList/ContactConnectionInfo.swift) | | | +| Contact connection view | [`ContactConnectionView.swift`](../../Shared/Views/ChatList/ContactConnectionView.swift) | | | +| Server summary | [`ServersSummaryView.swift`](../../Shared/Views/ChatList/ServersSummaryView.swift) | | | +| One-hand UI card | [`OneHandUICard.swift`](../../Shared/Views/ChatList/OneHandUICard.swift) | | | diff --git a/apps/ios/spec/client/chat-view.md b/apps/ios/spec/client/chat-view.md new file mode 100644 index 0000000000..b913287746 --- /dev/null +++ b/apps/ios/spec/client/chat-view.md @@ -0,0 +1,331 @@ +# SimpleX Chat iOS -- Chat View Module + +> Technical specification for the message rendering, chat item types, and context menu actions in the conversation view. +> +> Related specs: [Compose Module](compose.md) | [State Management](../state.md) | [API Reference](../api.md) | [README](../README.md) +> Related product: [Chat View](../../product/views/chat.md) + +**Source:** [`ChatView.swift`](../../Shared/Views/Chat/ChatView.swift) | [`ChatInfoView.swift`](../../Shared/Views/Chat/ChatInfoView.swift) | [`GroupChatInfoView.swift`](../../Shared/Views/Chat/Group/GroupChatInfoView.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatView](#2-chatview) +3. [ChatItemView -- Message Routing](#3-chatitemview) +4. [Message Renderers](#4-message-renderers) +5. [Media Views](#5-media-views) +6. [Metadata & Info](#6-metadata--info) +7. [Context Menu Actions](#7-context-menu-actions) +8. [Selection Mode](#8-selection-mode) + +--- + +## 1. Overview + +The chat view module renders individual conversations. It consists of: + +- **ChatView** -- The main conversation screen with message list, compose bar, and navigation +- **ChatItemView** -- Router that dispatches each chat item to the appropriate renderer +- **Specialized renderers** -- FramedItemView (standard messages), EmojiItemView (emoji-only), CICallItemView (calls), event views, etc. +- **Media views** -- CIImageView, CIVideoView, CIVoiceView, CIFileView for attachments + +``` +ChatView +├── Message List (ScrollView / LazyVStack) +│ ├── ChatItemView (per message) +│ │ ├── FramedItemView (text/media bubbles) +│ │ │ ├── MsgContentView (text with markdown) +│ │ │ ├── CIImageView / CIVideoView / CIVoiceView +│ │ │ └── CIMetaView (timestamp, status) +│ │ ├── EmojiItemView (emoji-only messages) +│ │ ├── CICallItemView (call events) +│ │ ├── CIEventView (system events) +│ │ ├── CIGroupInvitationView (group invitations) +│ │ ├── DeletedItemView / MarkedDeletedItemView +│ │ └── CIInvalidJSONView (decode errors) +│ └── ... (more items) +├── ComposeView (message input) +└── Navigation bar (contact/group info) +``` + +--- + +## [2. ChatView](../../Shared/Views/Chat/ChatView.swift#L18-L3135) + +**File**: [`Shared/Views/Chat/ChatView.swift`](../../Shared/Views/Chat/ChatView.swift) + +The main conversation view. Key responsibilities: + +### State +- Uses `ItemsModel.shared.reversedChatItems` for the primary message list +- `ChatModel.shared.chatId` identifies the active conversation +- Manages compose state, scroll position, keyboard visibility +- Tracks selection mode for multi-message actions + +### Message List +- Renders messages in a `ScrollViewReader` with `LazyVStack` +- Items are in reverse chronological order (newest at bottom) +- Supports infinite scroll: preloads older messages when scrolling up via `ItemsModel.preloadState` +- Handles pagination splits (`chatState.splits`) for non-contiguous loaded ranges + +### Navigation Bar +- Title: contact name / group name with connection status indicator +- Trailing button: navigates to [`ChatInfoView`](../../Shared/Views/Chat/ChatInfoView.swift#L93) (direct) or [`GroupChatInfoView`](../../Shared/Views/Chat/Group/GroupChatInfoView.swift#L16) (group) +- Search button: toggles in-chat message search + +### Scroll Behavior +- Auto-scrolls to bottom on new sent/received messages (if already near bottom) +- "Scroll to bottom" floating button when scrolled up +- `openAroundItemId` support: scrolls to a specific message (e.g., from search or notification) + +### Key Functions + +| Function | Line | Description | +|----------|------|-------------| +| [`body`](../../Shared/Views/Chat/ChatView.swift#L76) | L74 | Main view body | +| [`initChatView()`](../../Shared/Views/Chat/ChatView.swift#L675) | L672 | Initializes chat view state on appear | +| [`chatItemsList()`](../../Shared/Views/Chat/ChatView.swift#L821) | L814 | Builds the scrollable message list | +| [`scrollToItem(_:)`](../../Shared/Views/Chat/ChatView.swift#L735) | L731 | Scrolls to a specific message by ID | +| [`searchToolbar()`](../../Shared/Views/Chat/ChatView.swift#L769) | L764 | In-chat search toolbar UI | +| [`searchTextChanged(_:)`](../../Shared/Views/Chat/ChatView.swift#L1095) | L1087 | Handles search query changes | +| [`loadChatItems(_:_:)`](../../Shared/Views/Chat/ChatView.swift#L1531) | L1519 | Loads chat items with pagination | +| [`filtered(_:)`](../../Shared/Views/Chat/ChatView.swift#L807) | L801 | Filters items by content type | +| [`callButton(_:_:imageName:)`](../../Shared/Views/Chat/ChatView.swift#L1273) | L1264 | Audio/video call toolbar button | +| [`searchButton()`](../../Shared/Views/Chat/ChatView.swift#L1293) | L1284 | Search toggle toolbar button | +| [`addMembersButton()`](../../Shared/Views/Chat/ChatView.swift#L1361) | L1352 | Group add-members toolbar button | +| [`forwardSelectedMessages()`](../../Shared/Views/Chat/ChatView.swift#L1420) | L1409 | Forwards batch-selected messages | +| [`deletedSelectedMessages()`](../../Shared/Views/Chat/ChatView.swift#L1411) | L1401 | Deletes batch-selected messages | +| [`onChatItemsUpdated()`](../../Shared/Views/Chat/ChatView.swift#L1572) | L1559 | Reacts to chat items model changes | +| [`contentFilterMenu(withLabel:)`](../../Shared/Views/Chat/ChatView.swift#L1301) | L1292 | Content filter dropdown menu | + +### Supporting Types + +| Type | Line | Description | +|------|------|-------------| +| [`ChatItemWithMenu`](../../Shared/Views/Chat/ChatView.swift#L1600) | L1586 | Wraps each chat item with context menu | +| [`FloatingButtonModel`](../../Shared/Views/Chat/ChatView.swift#L2712) | L2697 | Manages scroll-to-bottom button state | +| [`ReactionContextMenu`](../../Shared/Views/Chat/ChatView.swift#L2899) | L2882 | Reaction picker context menu | +| [`ToggleNtfsButton`](../../Shared/Views/Chat/ChatView.swift#L2997) | L2980 | Mute/unmute notifications button | +| [`ContentFilter`](../../Shared/Views/Chat/ChatView.swift#L3049) | L3031 | Enum for message content filter types | +| [`deleteMessages()`](../../Shared/Views/Chat/ChatView.swift#L2795) | L2779 | Deletes messages with confirmation | +| [`archiveReports()`](../../Shared/Views/Chat/ChatView.swift#L2842) | L2826 | Archives report messages | + +--- + +## [3. ChatItemView](../../Shared/Views/Chat/ChatItemView.swift#L42) + +**File**: [`Shared/Views/Chat/ChatItemView.swift`](../../Shared/Views/Chat/ChatItemView.swift) + +Routes each `ChatItem` to the appropriate renderer based on its `CIContent` type: + +### Content Types (CIContent enum) + +| Content Type | Renderer | Line | Description | +|-------------|----------|------|-------------| +| `sndMsgContent` / `rcvMsgContent` | [`FramedItemView`](../../Shared/Views/Chat/ChatItem/FramedItemView.swift#L14) | L13 | Standard sent/received text+media message | +| `sndDeleted` / `rcvDeleted` | [`DeletedItemView`](../../Shared/Views/Chat/ChatItem/DeletedItemView.swift#L14) | L13 | Locally deleted message placeholder | +| `sndCall` / `rcvCall` | [`CICallItemView`](../../Shared/Views/Chat/ChatItem/CICallItemView.swift#L13) | L13 | Call event (missed, ended, duration) | +| `rcvIntegrityError` | [`IntegrityErrorItemView`](../../Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift#L14) | L13 | Message integrity error | +| `rcvDecryptionError` | [`CIRcvDecryptionError`](../../Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift#L16) | L15 | Decryption failure | +| `sndGroupInvitation` / `rcvGroupInvitation` | [`CIGroupInvitationView`](../../Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift#L14) | L13 | Group invite | +| `sndGroupEvent` / `rcvGroupEvent` | [`CIEventView`](../../Shared/Views/Chat/ChatItem/CIEventView.swift#L14) | L13 | Group system event | +| `rcvConnEvent` / `sndConnEvent` | [`CIEventView`](../../Shared/Views/Chat/ChatItem/CIEventView.swift#L14) | L13 | Connection event | +| `rcvChatFeature` / `sndChatFeature` | [`CIChatFeatureView`](../../Shared/Views/Chat/ChatItem/CIChatFeatureView.swift#L14) | L13 | Feature toggle event | +| `rcvChatPreference` / `sndChatPreference` | [`CIFeaturePreferenceView`](../../Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift#L14) | L13 | Preference change | +| `invalidJSON` | [`CIInvalidJSONView`](../../Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift#L14) | L13 | Failed to decode | + +### Bubble Direction +- Sent messages: aligned right, sender-colored bubble +- Received messages: aligned left, receiver-colored bubble +- Events/system messages: centered, no bubble + +### Appearance Dependencies +Each [`ChatItemWithMenu`](../../Shared/Views/Chat/ChatView.swift#L1600) may depend on the previous and next items for visual decisions: +- Whether to show the sender name (group messages, different sender than previous) +- Whether to show the tail on the bubble (last consecutive message from same sender) +- Date separator between messages on different days + +`ChatItemDummyModel.shared.sendUpdate()` forces a re-render of all items when global appearance changes. + +--- + +## 4. Message Renderers + +### [FramedItemView](../../Shared/Views/Chat/ChatItem/FramedItemView.swift#L14) + +**File**: [`Shared/Views/Chat/ChatItem/FramedItemView.swift`](../../Shared/Views/Chat/ChatItem/FramedItemView.swift) + +The standard message bubble. Renders: +- Quote/reply preview (if replying to another message) +- Forwarded indicator +- Sender name (in groups) +- Message content (`MsgContentView` with markdown) +- Attached media (image, video, voice, file, link preview) +- Reaction summary bar +- Metadata line (`CIMetaView`) + +### [EmojiItemView](../../Shared/Views/Chat/ChatItem/EmojiItemView.swift#L14) + +**File**: [`Shared/Views/Chat/ChatItem/EmojiItemView.swift`](../../Shared/Views/Chat/ChatItem/EmojiItemView.swift) + +Renders emoji-only messages (messages containing only emoji characters) in a larger font without a bubble background. + +### [MsgContentView](../../Shared/Views/Chat/ChatItem/MsgContentView.swift#L28) + +**File**: [`Shared/Views/Chat/ChatItem/MsgContentView.swift`](../../Shared/Views/Chat/ChatItem/MsgContentView.swift) + +Renders message text with SimpleX markdown formatting (bold, italic, code, links, mentions). + +### DeletedItemView / MarkedDeletedItemView + +**Files**: [`Shared/Views/Chat/ChatItem/DeletedItemView.swift`](../../Shared/Views/Chat/ChatItem/DeletedItemView.swift) | [`Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift`](../../Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift) + +- [`DeletedItemView`](../../Shared/Views/Chat/ChatItem/DeletedItemView.swift#L14): Placeholder for locally deleted messages +- [`MarkedDeletedItemView`](../../Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift#L14): Shows "message deleted" with optional moderation info (who deleted, when) + +### [CIEventView](../../Shared/Views/Chat/ChatItem/CIEventView.swift#L14) + +**File**: [`Shared/Views/Chat/ChatItem/CIEventView.swift`](../../Shared/Views/Chat/ChatItem/CIEventView.swift) + +Centered system event text for group events (member joined, left, role changed) and connection events. + +### [CIGroupInvitationView](../../Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift#L14) + +**File**: [`Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift`](../../Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift) + +Renders group invitation with accept/reject buttons. + +--- + +## 5. Media Views + +### [CIImageView](../../Shared/Views/Chat/ChatItem/CIImageView.swift#L14) + +**File**: [`Shared/Views/Chat/ChatItem/CIImageView.swift`](../../Shared/Views/Chat/ChatItem/CIImageView.swift) + +Renders inline images. Tapping opens `FullScreenMediaView` for zooming/panning. Images are compressed to `MAX_IMAGE_SIZE` (255KB) before sending. + +### [CIVideoView](../../Shared/Views/Chat/ChatItem/CIVideoView.swift#L16) + +**File**: [`Shared/Views/Chat/ChatItem/CIVideoView.swift`](../../Shared/Views/Chat/ChatItem/CIVideoView.swift) + +Renders video thumbnails with play button. Tapping opens video player. Videos above auto-receive threshold require manual download. + +### CIVoiceView / FramedCIVoiceView + +**Files**: [`Shared/Views/Chat/ChatItem/CIVoiceView.swift`](../../Shared/Views/Chat/ChatItem/CIVoiceView.swift) | [`Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift`](../../Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift) + +Renders voice messages with waveform visualization, play/pause control, and duration. [`FramedCIVoiceView`](../../Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift#L16) is the version inside a message bubble with additional context. + +### [CIFileView](../../Shared/Views/Chat/ChatItem/CIFileView.swift#L14) + +**File**: [`Shared/Views/Chat/ChatItem/CIFileView.swift`](../../Shared/Views/Chat/ChatItem/CIFileView.swift) + +Renders file attachments with filename, size, and download/open actions. Shows transfer progress during upload/download. + +### [CILinkView](../../Shared/Views/Chat/ChatItem/CILinkView.swift#L14) + +**File**: [`Shared/Views/Chat/ChatItem/CILinkView.swift`](../../Shared/Views/Chat/ChatItem/CILinkView.swift) + +Renders link preview cards with OpenGraph metadata (title, description, image). + +### [AnimatedImageView](../../Shared/Views/Chat/ChatItem/AnimatedImageView.swift#L11) + +**File**: [`Shared/Views/Chat/ChatItem/AnimatedImageView.swift`](../../Shared/Views/Chat/ChatItem/AnimatedImageView.swift) + +Renders animated GIF images. + +### [FullScreenMediaView](../../Shared/Views/Chat/ChatItem/FullScreenMediaView.swift#L16) + +**File**: [`Shared/Views/Chat/ChatItem/FullScreenMediaView.swift`](../../Shared/Views/Chat/ChatItem/FullScreenMediaView.swift) + +Full-screen media viewer with zoom, pan, and share actions. Supports images and videos. + +--- + +## 6. Metadata & Info + +### [CIMetaView](../../Shared/Views/Chat/ChatItem/CIMetaView.swift#L14) + +**File**: [`Shared/Views/Chat/ChatItem/CIMetaView.swift`](../../Shared/Views/Chat/ChatItem/CIMetaView.swift) + +Displays message metadata inline at the bottom of the bubble: +- Timestamp (sent time) +- Delivery status icon (sending, sent, delivered, read, error) +- Edit indicator (pencil icon if message was edited) +- Disappearing message timer (if timed message) + +### [ChatItemInfoView](../../Shared/Views/Chat/ChatItemInfoView.swift#L13) + +**File**: [`Shared/Views/Chat/ChatItemInfoView.swift`](../../Shared/Views/Chat/ChatItemInfoView.swift) + +Detailed message information sheet (accessed via long-press menu "Info"): +- Full delivery history (per-member delivery status in groups) +- Edit history (all previous versions of edited messages) +- Forward chain info +- Message timestamps (created, updated, deleted) + +--- + +## 7. Context Menu Actions + +Long-pressing a message shows a context menu with actions based on message type and ownership: + +| Action | Available For | API Command | +|--------|--------------|-------------| +| Reply | All messages | Sets compose state to `.replying` | +| Forward | Sent/received content messages | `apiForwardChatItems` | +| Copy | Text messages | Copies to clipboard | +| Edit | Own sent messages (within edit window) | `apiUpdateChatItem` | +| Delete for me | All messages | `apiDeleteChatItem(mode: .cidmInternal)` | +| Delete for everyone | Own sent messages | `apiDeleteChatItem(mode: .cidmBroadcast)` | +| Moderate | Group admin/owner for others' messages | `apiDeleteMemberChatItem` | +| React | Content messages (if reactions enabled) | `apiChatItemReaction` | +| Select | All messages | Enters multi-select mode | +| Info | All messages | Opens [`ChatItemInfoView`](../../Shared/Views/Chat/ChatItemInfoView.swift#L13) | +| Save | Media messages | Saves to photo library / files | +| Share | Content messages | iOS share sheet | + +--- + +## 8. Selection Mode + +Multi-selection mode allows batch operations on messages: + +- Enter via long-press "Select" action +- Toggle individual messages with tap +- Toolbar appears with batch actions: Delete, Forward +- Exit via cancel button or completing batch action + +--- + +## Source Files + +| File | Path | Line | +|------|------|------| +| Chat view | [`Shared/Views/Chat/ChatView.swift`](../../Shared/Views/Chat/ChatView.swift) | [L17](../../Shared/Views/Chat/ChatView.swift#L18) | +| Item router | [`Shared/Views/Chat/ChatItemView.swift`](../../Shared/Views/Chat/ChatItemView.swift) | [L41](../../Shared/Views/Chat/ChatItemView.swift#L42) | +| Framed bubble | [`Shared/Views/Chat/ChatItem/FramedItemView.swift`](../../Shared/Views/Chat/ChatItem/FramedItemView.swift) | [L13](../../Shared/Views/Chat/ChatItem/FramedItemView.swift#L14) | +| Emoji message | [`Shared/Views/Chat/ChatItem/EmojiItemView.swift`](../../Shared/Views/Chat/ChatItem/EmojiItemView.swift) | [L13](../../Shared/Views/Chat/ChatItem/EmojiItemView.swift#L14) | +| Image view | [`Shared/Views/Chat/ChatItem/CIImageView.swift`](../../Shared/Views/Chat/ChatItem/CIImageView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CIImageView.swift#L14) | +| Video view | [`Shared/Views/Chat/ChatItem/CIVideoView.swift`](../../Shared/Views/Chat/ChatItem/CIVideoView.swift) | [L15](../../Shared/Views/Chat/ChatItem/CIVideoView.swift#L16) | +| Voice view | [`Shared/Views/Chat/ChatItem/CIVoiceView.swift`](../../Shared/Views/Chat/ChatItem/CIVoiceView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CIVoiceView.swift#L14) | +| File view | [`Shared/Views/Chat/ChatItem/CIFileView.swift`](../../Shared/Views/Chat/ChatItem/CIFileView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CIFileView.swift#L14) | +| Link preview | [`Shared/Views/Chat/ChatItem/CILinkView.swift`](../../Shared/Views/Chat/ChatItem/CILinkView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CILinkView.swift#L14) | +| Call event | [`Shared/Views/Chat/ChatItem/CICallItemView.swift`](../../Shared/Views/Chat/ChatItem/CICallItemView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CICallItemView.swift#L13) | +| Metadata | [`Shared/Views/Chat/ChatItem/CIMetaView.swift`](../../Shared/Views/Chat/ChatItem/CIMetaView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CIMetaView.swift#L14) | +| Message info | [`Shared/Views/Chat/ChatItemInfoView.swift`](../../Shared/Views/Chat/ChatItemInfoView.swift) | [L12](../../Shared/Views/Chat/ChatItemInfoView.swift#L13) | +| System event | [`Shared/Views/Chat/ChatItem/CIEventView.swift`](../../Shared/Views/Chat/ChatItem/CIEventView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CIEventView.swift#L14) | +| Deleted placeholder | [`Shared/Views/Chat/ChatItem/DeletedItemView.swift`](../../Shared/Views/Chat/ChatItem/DeletedItemView.swift) | [L13](../../Shared/Views/Chat/ChatItem/DeletedItemView.swift#L14) | +| Moderated placeholder | [`Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift`](../../Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift) | [L13](../../Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift#L14) | +| Text content | [`Shared/Views/Chat/ChatItem/MsgContentView.swift`](../../Shared/Views/Chat/ChatItem/MsgContentView.swift) | [L27](../../Shared/Views/Chat/ChatItem/MsgContentView.swift#L28) | +| Group invitation | [`Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift`](../../Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift#L14) | +| Feature event | [`Shared/Views/Chat/ChatItem/CIChatFeatureView.swift`](../../Shared/Views/Chat/ChatItem/CIChatFeatureView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CIChatFeatureView.swift#L14) | +| Decryption error | [`Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift`](../../Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift) | [L15](../../Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift#L16) | +| Integrity error | [`Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift`](../../Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift) | [L13](../../Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift#L14) | +| Full-screen media | [`Shared/Views/Chat/ChatItem/FullScreenMediaView.swift`](../../Shared/Views/Chat/ChatItem/FullScreenMediaView.swift) | [L15](../../Shared/Views/Chat/ChatItem/FullScreenMediaView.swift#L16) | +| Animated image | [`Shared/Views/Chat/ChatItem/AnimatedImageView.swift`](../../Shared/Views/Chat/ChatItem/AnimatedImageView.swift) | [L10](../../Shared/Views/Chat/ChatItem/AnimatedImageView.swift#L11) | +| Framed voice | [`Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift`](../../Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift) | [L15](../../Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift#L16) | +| Member contact | [`Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift`](../../Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift) | [L13](../../Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift#L14) | diff --git a/apps/ios/spec/client/compose.md b/apps/ios/spec/client/compose.md new file mode 100644 index 0000000000..03116ddf6b --- /dev/null +++ b/apps/ios/spec/client/compose.md @@ -0,0 +1,355 @@ +# SimpleX Chat iOS -- Message Composition Module + +> Technical specification for the compose bar, attachment types, reply/edit/forward modes, voice recording, and mentions. +> +> Related specs: [Chat View](chat-view.md) | [File Transfer](../services/files.md) | [API Reference](../api.md) | [README](../README.md) +> Related product: [Chat View](../../product/views/chat.md) + +**Source:** [`ComposeView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ComposeView](#2-composeview) +3. [ComposeState Machine](#3-composestate-machine) +4. [Attachment Types](#4-attachment-types) +5. [Reply Mode](#5-reply-mode) +6. [Edit Mode](#6-edit-mode) +7. [Forward Mode](#7-forward-mode) +8. [Live Messages](#8-live-messages) +9. [Voice Recording](#9-voice-recording) +10. [Link Previews](#10-link-previews) +11. [Mentions](#11-mentions) + +--- + +## 1. Overview + +The compose module handles all message creation, editing, and forwarding. It sits at the bottom of `ChatView` and adapts its UI based on the current compose state. + +``` +ComposeView +├── Context banner (reply quote / edit indicator / forward indicator) +├── Attachment preview (image / video / file / voice waveform) +├── Text input (NativeTextEditor with markdown support) +├── Action buttons +│ ├── Attachment menu (camera, photo library, file picker) +│ ├── Voice record button (hold or toggle) +│ └── Send button (or live message indicator) +└── Link preview (auto-generated when URL detected) +``` + +--- + +## 2. [ComposeView](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L329) (`struct ComposeView: View`) + +**File**: `Shared/Views/Chat/ComposeMessage/ComposeView.swift` + +### Layout +- Fixed at the bottom of ChatView +- Expands vertically as text input grows (up to a maximum height) +- Context banner appears above the text field when in reply/edit/forward mode +- Attachment preview appears between context banner and text field + +### Key Properties +- Reads `ChatModel.shared.draft` / `draftChatId` for persisted drafts +- Manages its own internal compose state +- Coordinates with `ChatView` for scroll-to-bottom behavior on send + +### Send Flow +1. User taps send button +2. ComposeView constructs `[ComposedMessage]` from current state +3. Calls `apiSendMessages(type:, id:, scope:, live:, ttl:, composedMessages:)` +4. On success: clears compose state, scrolls to bottom +5. On failure: shows error alert, preserves compose state + +### Key Functions + +| Function | Line | Description | +|----------|------|-------------| +| [`body`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L369) | L360 | Main view body | +| [`sendMessageView()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L693) | L683 | Builds the send-message UI | +| [`sendMessage(ttl:)`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1106) | L1091 | Entry point: initiates send | +| [`sendMessageAsync()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1115) | L1099 | Async send implementation | +| [`clearState(live:)`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1467) | L1446 | Resets compose state after send | +| [`addMediaContent()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L893) | L882 | Adds media attachment | +| [`connectCheckLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L866) | L856 | Checks link preview before connect | +| [`commandsButton()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L754) | L744 | Builds commands menu button | + +### Draft Persistence + +| Function | Line | Description | +|----------|------|-------------| +| [`saveCurrentDraft()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1481) | L1459 | Saves compose state to `ChatModel.draft` | +| [`clearCurrentDraft()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1487) | L1464 | Clears persisted draft | + +- When navigating away from a chat, compose state is saved to `ChatModel.draft` / `ChatModel.draftChatId` +- When returning to the same chat, draft is restored +- Drafts are not persisted across app restarts + +--- + +## 3. [ComposeState](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L45) Machine (`struct ComposeState`) + +The compose bar operates as a state machine with these primary states: + +``` + ┌──────────┐ + │ .empty │ ← initial / after send + └─────┬────┘ + │ user types / attaches / quotes + v + ┌─────────────────────────────────────┐ + │ │ + ┌────▼────┐ ┌──────────────┐ ┌──────────▼───┐ + │ .text │ │ .mediaPending │ │ .voiceRecording │ + └─────────┘ └──────────────┘ └───────────────┘ + │ │ + │ long-press reply│ tap edit + v v + ┌──────────┐ ┌──────────┐ ┌───────────┐ + │ .replying │ │ .editing │ │ .forwarding│ + └──────────┘ └──────────┘ └───────────┘ +``` + +### Supporting Types + +| Type | Line | Description | +|------|------|-------------| +| [`enum ComposePreview`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L11) | L10 | Preview variants (image, voice, file, etc.) | +| [`enum ComposeContextItem`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L20) | L18 | Context item for reply/quote | +| [`enum VoiceMessageRecordingState`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L29) | L26 | Recording state enum | +| [`struct ComposeState`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L45) | L40 | Full compose state struct | +| [`copy()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L98) | L93 | Copy compose state with overrides | +| [`mentionMemberName()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L118) | L113 | Format mention display name | +| [`chatItemPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L266) | L260 | Build preview from chat item | +| [`enum UploadContent`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L287) | L280 | Upload content variants | + +### States + +| State | Description | UI | +|-------|-------------|-----| +| `.empty` | No input, no attachments | Placeholder text, attachment button | +| `.text` | Text entered, no attachments | Send button visible | +| `.mediaPending` | Media/file selected, optionally with text | Preview visible, send button | +| `.voiceRecording` | Voice recording in progress | Waveform animation, stop/send | +| `.replying` | Replying to a specific message | Quote banner above input | +| `.editing` | Editing a previously sent message | Edit banner, pre-filled text | +| `.forwarding` | Forwarding selected messages | Forward banner, item previews | + +### Transitions + +| From | Trigger | To | +|------|---------|-----| +| `.empty` | User types text | `.text` | +| `.empty` | User selects media | `.mediaPending` | +| `.empty` | User holds voice button | `.voiceRecording` | +| `.empty` | User long-presses message "Reply" | `.replying` | +| `.empty` | User long-presses message "Edit" | `.editing` | +| `.empty` | User selects "Forward" | `.forwarding` | +| Any | User taps send | `.empty` | +| Any | User taps cancel (X) | `.empty` | + +--- + +## 4. Attachment Types + +### [ComposeImageView](../../Shared/Views/Chat/ComposeMessage/ComposeImageView.swift#L12) + +**File**: [`ComposeImageView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeImageView.swift) (struct at L12) + +Preview of selected image(s) before sending. Shows thumbnail with remove button. Images are compressed to `MAX_IMAGE_SIZE` (255KB) before sending. + +### [ComposeFileView](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift#L11) + +**File**: [`ComposeFileView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift) (struct at L11) + +Preview of selected file or video. Shows filename, size, and remove button. Videos show a thumbnail frame. + +### [ComposeVoiceView](../../Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift#L26) + +**File**: [`ComposeVoiceView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift) (struct at L26) + +Voice message recording/playback preview. Shows waveform visualization, duration, and play/delete buttons. + +### Attachment Menu Options + +| Option | Picker | Max Size | Transfer Method | +|--------|--------|----------|-----------------| +| Camera photo | UIImagePickerController | Compressed to 255KB | Inline in SMP message | +| Photo library | PHPickerViewController | Compressed to 255KB | Inline or XFTP | +| Video | PHPickerViewController | Up to 1GB | XFTP | +| File | UIDocumentPickerViewController | Up to 1GB | XFTP | + +--- + +## 5. Reply Mode + +Activated via long-press context menu "Reply" on any message. + +### UI +- Quote banner above text input showing original message preview +- X button to cancel reply +- Original message reference stored in compose state + +### API +- Reply is sent as part of `ComposedMessage` with `quotedItemId` parameter +- `apiSendMessages(composedMessages: [ComposedMessage(quotedItemId: originalItem.id, ...)])` + +--- + +## 6. Edit Mode + +Activated via long-press context menu "Edit" on own sent messages (within the edit window). + +### UI +- Edit banner above text input with pencil icon +- Text field pre-filled with original message content +- Send button changes to "Save" / checkmark + +### API +- `apiUpdateChatItem(type:, id:, scope:, itemId:, updatedMessage:, live:)` +- Response: `ChatResponse1.chatItemUpdated(user:, chatItem:)` + +### Constraints +- Only own sent messages can be edited +- Edit is available within a server-defined time window +- Edited messages show a pencil indicator in `CIMetaView` +- Edit history is visible in `ChatItemInfoView` + +--- + +## 7. Forward Mode + +Activated via long-press context menu "Forward" or via multi-select toolbar. + +### Flow +1. User selects "Forward" on message(s) +2. `apiPlanForwardChatItems(fromChatType:, fromChatId:, fromScope:, itemIds:)` is called to plan +3. Response: `ChatResponse1.forwardPlan(user:, chatItemIds:, forwardConfirmation:)` +4. User selects destination chat +5. `apiForwardChatItems(toChatType:, toChatId:, toScope:, fromChatType:, fromChatId:, fromScope:, itemIds:, ttl:)` executes the forward +6. Forwarded messages appear with a forwarded indicator + +### ForwardConfirmation +The plan response may include a `forwardConfirmation` requiring user confirmation (e.g., forwarding to a less secure chat). + +--- + +## 8. [Live Messages](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L36) (`struct LiveMessage`) + +Optional feature where the recipient sees typing in real-time. + +### How It Works +- User enables live message mode (lightning icon) +- As user types, `apiSendMessages(live: true)` is called repeatedly +- Each call sends the current text as an update to the same message +- Recipient sees the message being composed in real-time +- Final send marks the message as complete + +### Key Functions + +| Function | Line | Description | +|----------|------|-------------| +| [`sendLiveMessage()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L922) | L910 | Initiates a live message | +| [`updateLiveMessage()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L940) | L927 | Sends incremental live update | +| [`liveMessageToSend()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L959) | L945 | Determines text diff to send | +| [`truncateToWords()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L964) | L950 | Truncates text at word boundary | + +### API +- Initial: `apiSendMessages(live: true, composedMessages: [...])` -- creates live message +- Updates: `apiUpdateChatItem(live: true)` -- updates content as user types +- Final: `apiUpdateChatItem(live: false)` -- marks as complete + +--- + +## 9. Voice Recording + +### Recording Flow +1. User taps (or holds) the microphone button +2. `AVAudioRecorder` starts recording in compressed format +3. Waveform visualization shows real-time audio levels +4. User taps stop (or releases hold) to finish recording +5. Preview with playback shown in compose area +6. User taps send to deliver + +### Voice Functions + +| Function | Line | Description | +|----------|------|-------------| +| [`startVoiceMessageRecording()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1382) | L1365 | Begins audio recording | +| [`finishVoiceMessageRecording()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1423) | L1405 | Stops recording, shows preview | +| [`allowVoiceMessagesToContact()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1434) | L1415 | Enables voice messages for contact | +| [`updateComposeVMRFinished()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1441) | L1422 | Updates state after recording finishes | +| [`cancelCurrentVoiceRecording()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1453) | L1434 | Cancels in-progress recording | +| [`cancelVoiceMessageRecording()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1460) | L1440 | Cancels and cleans up recording file | + +### Constraints +- Maximum duration: `MAX_VOICE_MESSAGE_LENGTH = 300` seconds (5 minutes) +- Auto-receive threshold: `MAX_VOICE_SIZE_AUTO_RCV = 522,240` bytes (510KB) +- Compressed audio format for small file sizes + +### Audio Management +- [`AudioRecorder`](../../Shared/Model/AudioRecPlay.swift#L14) (`Shared/Model/AudioRecPlay.swift` L14) manages recording and playback +- `ChatModel.stopPreviousRecPlay` coordinates exclusive audio playback (only one audio source plays at a time) + +--- + +## 10. [Link Previews](../../Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift#L13) (`ComposeLinkView`) + +**File**: [`ComposeLinkView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift) (struct at L13) + +### Auto-Detection +- As user types, URLs in the text are detected +- When a URL is found, `ComposeLinkView` fetches OpenGraph metadata +- Preview card shows title, description, and thumbnail image + +### Link Preview Functions + +| Function | Line | Description | +|----------|------|-------------| +| [`showLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1495) | L1471 | Triggers link preview loading | +| [`getMessageLinks()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1515) | L1490 | Extracts URLs from formatted text | +| [`isSimplexLink()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1526) | L1501 | Checks if URL is a SimpleX link | +| [`cancelLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1530) | L1505 | Cancels pending preview | +| [`loadLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1542) | L1516 | Fetches OpenGraph metadata | +| [`resetLinkPreview()`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L1559) | L1533 | Resets preview state | + +### Behavior +- Only the first URL in the message generates a preview +- Preview can be dismissed by the user +- Link preview data is included in the `ComposedMessage` sent to the core +- Toggle in privacy settings to disable auto-preview generation + +--- + +## 11. Mentions + +In group chats, typing `@` triggers member name autocomplete: + +### Flow +1. User types `@` in the text field +2. Autocomplete dropdown appears with matching group members +3. User selects a member +4. `@displayName` is inserted into the text +5. Mention is rendered with special formatting in the sent message + +### Data +- Group members loaded from `ChatModel.groupMembers` +- Mention metadata included in `ComposedMessage` + +--- + +## Source Files + +| File | Path | Struct/Class | Line | +|------|------|--------------|------| +| Compose view | [`ComposeView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift) | `ComposeView` | [L321](../../Shared/Views/Chat/ComposeMessage/ComposeView.swift#L329) | +| Send message UI | [`SendMessageView.swift`](../../Shared/Views/Chat/ComposeMessage/SendMessageView.swift) | `SendMessageView` | [L14](../../Shared/Views/Chat/ComposeMessage/SendMessageView.swift#L15) | +| Image preview | [`ComposeImageView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeImageView.swift) | `ComposeImageView` | [L12](../../Shared/Views/Chat/ComposeMessage/ComposeImageView.swift#L12) | +| File preview | [`ComposeFileView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift) | `ComposeFileView` | [L11](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift#L11) | +| Voice preview | [`ComposeVoiceView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift) | `ComposeVoiceView` | [L26](../../Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift#L26) | +| Link preview | [`ComposeLinkView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift) | `ComposeLinkView` | [L13](../../Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift#L13) | +| Audio recording | [`AudioRecPlay.swift`](../../Shared/Model/AudioRecPlay.swift) | `AudioRecorder` | [L14](../../Shared/Model/AudioRecPlay.swift#L14) | diff --git a/apps/ios/spec/client/navigation.md b/apps/ios/spec/client/navigation.md new file mode 100644 index 0000000000..e755115827 --- /dev/null +++ b/apps/ios/spec/client/navigation.md @@ -0,0 +1,312 @@ +# SimpleX Chat iOS -- Navigation Architecture + +> Technical specification for the navigation stack, deep linking, sheet presentation, and call overlay. +> +> Related specs: [Chat List](chat-list.md) | [Chat View](chat-view.md) | [State Management](../state.md) | [README](../README.md) +> Related product: [Product Overview](../../product/README.md) + +**Source:** [`ContentView.swift`](../../Shared/ContentView.swift) | [`NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift) | [`SettingsView.swift`](../../Shared/Views/UserSettings/SettingsView.swift) | [`OnboardingView.swift`](../../Shared/Views/Onboarding/OnboardingView.swift) | [`UserProfilesView.swift`](../../Shared/Views/UserSettings/UserProfilesView.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Root View -- ContentView](#2-root-view) +3. [Navigation Stack](#3-navigation-stack) +4. [Sheet Presentation](#4-sheet-presentation) +5. [Deep Linking](#5-deep-linking) +6. [Call Overlay](#6-call-overlay) +7. [Authentication Gate](#7-authentication-gate) +8. [Onboarding Flow](#8-onboarding-flow) + +--- + +## 1. Overview + +The app's navigation follows a hierarchical model with a single navigation stack rooted in `ContentView`. Modal sheets and full-screen overlays augment the primary navigation path. + +``` +SimpleXApp +└── ContentView (root) + ├── Authentication gate (LocalAuthView / SetAppPasscodeView) + ├── Onboarding flow (if first launch / migration) + ├── Main content + │ └── NavigationStack / NavigationView + │ ├── ChatListView (root of stack) + │ │ ├── ChatView (pushed) + │ │ │ ├── ChatInfoView / GroupChatInfoView (pushed) + │ │ │ └── ChatItemInfoView (pushed) + │ │ └── ContactConnectionInfo (pushed) + │ └── Settings views (pushed) + ├── Sheets (modal) + │ ├── UserPicker + │ ├── NewChatView + │ ├── WhatsNew / Notices + │ └── Settings sub-views + └── Overlays (always on top) + ├── Active call banner (when call active) + └── ActiveCallView (full-screen call) +``` + +--- + +## 2. Root View -- [`ContentView`](../../Shared/ContentView.swift#L24) + +**File**: [`Shared/ContentView.swift`](../../Shared/ContentView.swift) + +`ContentView` is the root view injected by `SimpleXApp`. It manages: + +### [Environment](../../Shared/ContentView.swift#L25-L37) +- `@EnvironmentObject var chatModel: ChatModel` +- `@EnvironmentObject var theme: AppTheme` +- `@Environment(\.scenePhase) var scenePhase` + +### [Key State](../../Shared/ContentView.swift#L35-L52) +| Property | Type | Purpose | +|----------|------|---------| +| [`contentAccessAuthenticationExtended`](../../Shared/ContentView.swift#L35) | `Bool` | Passed at init to avoid re-render timing issues | +| [`automaticAuthenticationAttempted`](../../Shared/ContentView.swift#L38) | `Bool` | Whether biometric auth was auto-attempted | +| [`waitingForOrPassedAuth`](../../Shared/ContentView.swift#L51) | `Bool` | Whether auth gate should show | +| [`chatListUserPickerSheet`](../../Shared/ContentView.swift#L52) | `UserPickerSheet?` | Active user picker sheet | + +### [View Selection Logic](../../Shared/ContentView.swift#L60-L80) + +```swift +// Simplified decision tree in ContentView.body: +if !prefPerformLA || accessAuthenticated { + contentView() // Main app content +} else { + lockButton() // Authentication required +} +``` + +The [`contentView()`](../../Shared/ContentView.swift#L169) function further decides: +- If `chatModel.onboardingStage != .onboardingComplete`: show [onboarding](../../Shared/ContentView.swift#L174) +- If `chatModel.migrationState != nil`: show migration UI +- Otherwise: show `ChatListView` in a navigation container + +--- + +## 3. Navigation Stack + +### iOS Version Compatibility + +**File**: [`Shared/Views/Helpers/NavStackCompat.swift`](../../Shared/Views/Helpers/NavStackCompat.swift) + +The app supports iOS 15+ and uses a compatibility wrapper ([`NavStackCompat`](../../Shared/Views/Helpers/NavStackCompat.swift#L11)): + +```swift +// NavStackCompat provides: +// - NavigationStack (iOS 16+): programmatic navigation via NavigationPath +// - NavigationView (iOS 15): classic NavigationLink-based navigation +``` + +### Primary Navigation Path + +``` +ChatListView + │ + ├─[tap chat]─→ ChatView + │ │ + │ ├─[tap info]─→ ChatInfoView (direct) + │ │ └─→ VerifyCodeView, etc. + │ │ + │ ├─[tap info]─→ GroupChatInfoView (group) + │ │ ├─→ GroupMemberInfoView + │ │ ├─→ GroupProfileView + │ │ └─→ GroupLinkView + │ │ + │ └─[tap message info]─→ ChatItemInfoView + │ + ├─[tap connection]─→ ContactConnectionInfo + │ + └─[settings]─→ SettingsView + ├─→ NotificationsView + ├─→ NetworkAndServers + ├─→ AppearanceSettings + ├─→ PrivacySettings + ├─→ DatabaseView + └─→ UserProfilesView +``` + +### Navigation Trigger + +Chat navigation is triggered by setting `ChatModel.chatId`: + +```swift +// In ChatListNavLink: +ItemsModel.shared.loadOpenChat(chatId) { + // This sets ChatModel.chatId = chatId after a 250ms delay + // allowing navigation animation to start smoothly +} +``` + +--- + +## 4. Sheet Presentation + +Sheets are presented modally on top of the navigation stack: + +| Sheet | Trigger | Content | +|-------|---------|---------| +| UserPicker | Tap user avatar in nav bar | User list, settings shortcuts | +| [`NewChatView`](../../Shared/Views/NewChat/NewChatView.swift#L78) | Tap FAB / "+" button | Create link, scan QR, paste link, new group | +| WhatsNew | App update detected | Release notes | +| AddGroupView | "New Group" action | Group creation wizard | +| ConnectDesktopView | Settings > Desktop | Remote desktop pairing | +| MigrateFromDevice | Settings > Migration | Device export | +| MigrateToDevice | Onboarding migration | Device import | +| [LocalAuthView](../../Shared/ContentView.swift#L95) | App foreground after background | Biometric/passcode auth | + +### Sheet Management + +Sheets use SwiftUI `.sheet(item:)` or `.sheet(isPresented:)` modifiers on `ContentView` and `ChatListView`. Some sheets use the centralized [`AppSheetState.shared`](../../Shared/ContentView.swift#L29) observable for coordination: + +```swift +class AppSheetState: ObservableObject { + static let shared = AppSheetState() + var scenePhaseActive: Bool = false + // ... sheet state coordination +} +``` + +--- + +## 5. Deep Linking + +### Notification Deep Link + +When the user taps a notification: + +1. `NtfManager.processNotificationResponse()` extracts the `chatId` from notification payload +2. If a different user: calls `changeActiveUser(userId:)` +3. Sets `ChatModel.chatId = chatId` to navigate to the conversation +4. If the app was in background: the notification response is stored in `ChatModel.notificationResponse` and processed when the app becomes active + +### [URL Deep Link](../../Shared/ContentView.swift#L281) + +SimpleX links (`simplex:/chat#...`) are handled via [`connectViaUrl()`](../../Shared/ContentView.swift#L439): + +```swift +.onOpenURL { url in + if AppChatState.shared.value == .active { + chatModel.appOpenUrl = url // Process immediately + } else { + chatModel.appOpenUrlLater = url // Process when active + } +} +``` + +URL processing routes to the appropriate connection flow (join group, add contact, etc.) via [`planAndConnect()`](../../Shared/Views/NewChat/NewChatView.swift#L1169). + +### Call Deep Link + +Call invitations from notifications: +1. `NtfManager` detects `ntfActionAcceptCall` action +2. Sets `ChatModel.ntfCallInvitationAction = (chatId, .accept)` +3. `ContentView` picks up the pending action and initiates the call + +--- + +## 6. Call Overlay + +The call UI overlays the entire app when a call is active: + +### [Call Banner](../../Shared/ContentView.swift#L203) + +When `ChatModel.activeCall != nil` and call is in connecting/active state: +- A banner appears at the top of ContentView (height: [`callTopPadding = 40`](../../Shared/ContentView.swift#L54)) +- Shows contact name, call duration, tap to return to full-screen call +- Main content is padded down to accommodate the banner + +### [Full-Screen Call View](../../Shared/ContentView.swift#L185) + +When `ChatModel.showCallView == true`: +- `ActiveCallView` covers the entire screen as a ZStack overlay +- Contains local/remote video, controls (mute, camera, speaker, end) +- PiP mode: `ChatModel.activeCallViewIsCollapsed` collapses to mini view +- Call view is always rendered on top of navigation and sheets + +```swift +// In ContentView.allViews(): +ZStack { + contentView() + .padding(.top, showCallArea ? callTopPadding : 0) + + if showCallArea, let call = chatModel.activeCall { + VStack { + activeCallInteractiveArea(call) + Spacer() + } + } + + if chatModel.showCallView, let call = chatModel.activeCall { + callView(call) // Full screen overlay + } +} +``` + +--- + +## 7. Authentication Gate + +### [Local Authentication](../../Shared/ContentView.swift#L359) + +When [`DEFAULT_PERFORM_LA`](../../Shared/ContentView.swift#L44) is enabled: + +1. App enters background: `chatModel.contentViewAccessAuthenticated = false` +2. App returns to foreground: `ContentView` shows [`lockButton()`](../../Shared/ContentView.swift#L238) instead of content +3. User taps lock button: [`LocalAuthView`](../../Shared/ContentView.swift#L95) presented +4. On successful auth: `chatModel.contentViewAccessAuthenticated = true`, content revealed + +### Authentication Methods +- Face ID / Touch ID (via `LocalAuthentication` framework) +- Custom numeric passcode +- Custom alphanumeric passcode + +### [Extended Authentication](../../Shared/ContentView.swift#L351) +- After successful auth, a grace period prevents re-auth for brief background/foreground cycles ([`unlockedRecently()`](../../Shared/ContentView.swift#L351)) +- [`contentAccessAuthenticationExtended`](../../Shared/ContentView.swift#L35) is computed at `ContentView.init` to avoid render-time race conditions +- The `enteredBackgroundAuthenticated` timestamp tracks when the app was last authenticated in background + +--- + +## 8. [Onboarding Flow](../../Shared/Views/Onboarding/OnboardingView.swift#L13) + +First-launch experience controlled by [`ChatModel.onboardingStage`](../../Shared/Views/Onboarding/OnboardingView.swift#L46): + +```swift +enum OnboardingStage: String, Identifiable { + case step1_SimpleXInfo // Welcome screen + case step2_CreateProfile // deprecated + case step3_CreateSimpleXAddress // deprecated + case step3_ChooseServerOperators // Choose server operators + case step4_SetNotificationsMode // Set notification preferences + case onboardingComplete // Normal operation +} +``` + +Each stage is a dedicated view presented in place of `ChatListView` within [`ContentView`](../../Shared/ContentView.swift#L174). + +Migration state (`ChatModel.migrationState != nil`) takes precedence over onboarding. + +--- + +## Source Files + +| File | Path | +|------|------| +| Root view | [`Shared/ContentView.swift`](../../Shared/ContentView.swift) | +| App entry point | `Shared/SimpleXApp.swift` | +| Navigation compat | [`Shared/Views/Helpers/NavStackCompat.swift`](../../Shared/Views/Helpers/NavStackCompat.swift) | +| Chat list (nav root) | `Shared/Views/ChatList/ChatListView.swift` | +| Nav link wrapper | `Shared/Views/ChatList/ChatListNavLink.swift` | +| User picker | `Shared/Views/ChatList/UserPicker.swift` | +| New chat view | [`Shared/Views/NewChat/NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift) | +| Settings view | [`Shared/Views/UserSettings/SettingsView.swift`](../../Shared/Views/UserSettings/SettingsView.swift) | +| User profiles | [`Shared/Views/UserSettings/UserProfilesView.swift`](../../Shared/Views/UserSettings/UserProfilesView.swift) | +| Onboarding view | [`Shared/Views/Onboarding/OnboardingView.swift`](../../Shared/Views/Onboarding/OnboardingView.swift) | +| Active call view | `Shared/Views/Call/ActiveCallView.swift` | +| Local auth view | `Shared/Views/LocalAuth/LocalAuthView.swift` | +| Notification manager | `Shared/Model/NtfManager.swift` | diff --git a/apps/ios/spec/database.md b/apps/ios/spec/database.md new file mode 100644 index 0000000000..9e5adfcb64 --- /dev/null +++ b/apps/ios/spec/database.md @@ -0,0 +1,298 @@ +# SimpleX Chat iOS -- Database & Storage + +**Source:** [`FileUtils.swift`](../SimpleXChat/FileUtils.swift) + +> Technical specification for the database architecture, encryption, file storage, and export/import functionality. +> +> Related specs: [Architecture](architecture.md) | [State Management](state.md) | [README](README.md) +> Related product: [Product Overview](../product/README.md) + +--- + +## Table of Contents + +1. [Database Overview](#1-database-overview) +2. [Database Files & Paths](#2-database-files--paths) +3. [Haskell Store Modules](#3-haskell-store-modules) +4. [Migrations](#4-migrations) +5. [Database Encryption](#5-database-encryption) +6. [File Storage](#6-file-storage) +7. [Export & Import](#7-export--import) +8. [App Group Sharing](#8-app-group-sharing) + +--- + +## 1. Database Overview + +SimpleX Chat uses two SQLite databases managed entirely by the Haskell core. The iOS Swift layer never reads or writes directly to the databases -- all data access goes through the FFI command/response API. + +| Database | Suffix | Contents | +|----------|--------|----------| +| Chat DB | `_chat.db` | Messages, contacts, groups, user profiles, files, tags, preferences, call history | +| Agent DB | `_agent.db` | SMP agent connections, cryptographic keys, message queues, server state, XFTP chunks | + +Both databases are initialized and migrated via the C FFI function `chat_migrate_init_key()`, which applies pending migrations and returns a `chat_ctrl` pointer. + +--- + +## 2. Database Files & Paths + +### [Path Resolution](../SimpleXChat/FileUtils.swift#L63-L73) (FileUtils.swift) + +```swift +let DB_FILE_PREFIX = "simplex_v1" + +// Database path depends on container preference +func getAppDatabasePath() -> URL { + dbContainerGroupDefault.get() == .group + ? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX) + : getLegacyDatabasePath() +} + +// Full database file paths: +// Chat: {container}/simplex_v1_chat.db +// Agent: {container}/simplex_v1_agent.db +``` + +### [File Constants](../SimpleXChat/FileUtils.swift#L38-L44) + +```swift +let CHAT_DB: String = "_chat.db" +let AGENT_DB: String = "_agent.db" +private let CHAT_DB_BAK: String = "_chat.db.bak" +private let AGENT_DB_BAK: String = "_agent.db.bak" +``` + +### Container Locations + +See [`getDocumentsDirectory()`](../SimpleXChat/FileUtils.swift#L47) and [`getGroupContainerDirectory()`](../SimpleXChat/FileUtils.swift#L52). + +| Container | Path | Used When | +|-----------|------|-----------| +| App Group | `FileManager.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)` | Default (shared with NSE) | +| Documents | `FileManager.urls(for: .documentDirectory)` | Legacy installations | + +The container choice is stored in `dbContainerGroupDefault` (`GroupDefaults`). + +--- + +## 3. Haskell Store Modules + +All database operations are implemented in Haskell. Key store modules (paths relative to repo root): + +| Module | Path | Size | Description | +|--------|------|------|-------------| +| Messages | `src/Simplex/Chat/Store/Messages.hs` | ~178KB | Message CRUD, pagination, search, reactions, delivery receipts | +| Groups | `src/Simplex/Chat/Store/Groups.hs` | ~126KB | Group CRUD, member management, roles, links, invitations | +| Direct | `src/Simplex/Chat/Store/Direct.hs` | ~52KB | Direct contact connections, contact requests. See `createDirectChat` in `Store/Direct.hs` | +| Files | `src/Simplex/Chat/Store/Files.hs` | ~43KB | File transfer state, XFTP chunks, inline files | +| Profiles | `src/Simplex/Chat/Store/Profiles.hs` | ~42KB | User profiles, contact profiles, incognito profiles | +| Connections | `src/Simplex/Chat/Store/Connections.hs` | ~17KB | Connection lifecycle, queue management | + +### Data Model (key tables) + +``` +users -- User profiles (userId, displayName, fullName, image, ...) +contacts -- Contact records (contactId, userId, localDisplayName, ...) +groups -- Group records (groupId, userId, groupProfile, ...) +group_members -- Group membership (groupMemberId, groupId, memberId, role, ...) +messages -- Message records (messageId, chatItemId, msgBody, ...) +chat_items -- Chat items (chatItemId, chatType, chatId, content, ...) +files -- File transfer records (fileId, chatItemId, fileName, fileSize, ...) +connections -- SMP connections (connId, agentConnId, ...) +chat_tags -- User-defined chat tags +chat_tags_chats -- Tag-to-chat assignments +``` + +--- + +## 4. Migrations + +Database migrations are managed by the Haskell core. Migration files are located in: + +``` +src/Simplex/Chat/Store/SQLite/Migrations/ +``` + +Migrations are numbered sequentially starting from `M20220101` through `M20260122` (200+ migrations). Each migration is a Haskell module containing SQL statements for schema changes. + +The migration process: +1. `chat_migrate_init_key()` is called with the database path +2. Haskell reads the current schema version from the database +3. Pending migrations are applied in order +4. If migration fails, the function returns an error string (not a `chat_ctrl`) +5. On success, a `chat_ctrl` pointer is returned + +Migration results are decoded in Swift as `DBMigrationResult`: +- `.ok` -- migrations applied successfully +- `.invalidConfirmation` -- migration requires user confirmation +- `.errorNotADatabase(dbFile:)` -- file is not a valid SQLite database +- `.errorMigration(dbFile:, migrationError:)` -- migration failed +- `.errorSQL(dbFile:, migrationSQLError:)` -- SQL error during migration +- `.errorKeychain` -- keychain access failed +- `.unknown(json:)` -- unrecognized response + +--- + +## 5. Database Encryption + +### Encryption Configuration + +Database encryption uses SQLCipher (AES-256) and is managed through the API: + +```swift +// Set or change encryption +ChatCommand.apiStorageEncryption(config: DBEncryptionConfig) + +// Test if a key is correct +ChatCommand.testStorageEncryption(key: String) +``` + +`DBEncryptionConfig` contains: +- `currentKey: String` -- current encryption key (empty if unencrypted) +- `newKey: String` -- new encryption key (empty to decrypt) + +### Key Storage + +The encryption key is stored in the iOS Keychain via `kcDatabasePassword`: +- On first launch with encryption, the key is generated and stored +- The `storeDBPassphraseGroupDefault` flag controls whether the key is auto-stored +- If the user opts out of auto-storage, they must enter the key on each launch + +### UI + +- [`DatabaseEncryptionView.swift`](../Shared/Views/Database/DatabaseEncryptionView.swift) -- Encryption settings UI +- [`DatabaseView.swift`](../Shared/Views/Database/DatabaseView.swift) -- Database management UI (size, export, import, encryption) + +--- + +## 6. File Storage + +### Directory Structure + +``` +{App Container}/ +├── Documents/ +│ ├── app_files/ -- Downloaded and sent files +│ ├── temp_files/ -- Temporary files during transfer +│ └── assets/wallpapers/ -- Custom wallpaper images +├── {App Group Container}/ +│ ├── simplex_v1_chat.db -- Chat database +│ ├── simplex_v1_agent.db -- Agent database +│ └── ... +``` + +### [File Size Constants](../SimpleXChat/FileUtils.swift#L18-L36) (FileUtils.swift) + +```swift +public let MAX_IMAGE_SIZE: Int64 = 261_120 // 255 KB -- inline image compression target +public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = 522_240 // 510 KB -- auto-receive images +public let MAX_VOICE_SIZE_AUTO_RCV: Int64 = 522_240 // 510 KB -- auto-receive voice +public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023 KB -- auto-receive video +public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1 GB -- max XFTP transfer +public let MAX_FILE_SIZE_SMP: Int64 = 8_000_000 // ~7.6 MB -- max SMP inline +public let MAX_FILE_SIZE_LOCAL: Int64 = Int64.max // No limit for local files +public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(300) // 5 minutes +``` + +### CryptoFile (Encrypted File Storage) + +When `apiSetEncryptLocalFiles(enable: true)` is set, files stored on device are AES-encrypted: + +- Encryption/decryption uses `chat_encrypt_file` / `chat_decrypt_file` C FFI functions +- Each file gets a unique key and nonce stored alongside the file reference +- The `CryptoFile` type wraps `(filePath: String, cryptoArgs: CryptoFileArgs?)` where `CryptoFileArgs` contains `(fileKey: String, fileNonce: String)` + +### [File Path Helpers](../SimpleXChat/FileUtils.swift#L219-L221) + +```swift +public func getDocumentsDirectory() -> URL // Standard documents dir +public func getGroupContainerDirectory() -> URL // App group container +func getAppFilesDirectory() -> URL // {appDir}/app_files/ +func getTempFilesDirectory() -> URL // {appDir}/temp_files/ +func getWallpaperDirectory() -> URL // {appDir}/assets/wallpapers/ +``` + +See also [`saveFile()`](../SimpleXChat/FileUtils.swift#L226), [`removeFile()`](../SimpleXChat/FileUtils.swift#L243), and [`getMaxFileSize()`](../SimpleXChat/FileUtils.swift#L276). + +### [Cleanup](../SimpleXChat/FileUtils.swift#L86-L116) + +- Files are deleted when their associated `ChatItem` is deleted. See [`cleanupFile()`](../SimpleXChat/FileUtils.swift#L267) and [`cleanupDirectFile()`](../SimpleXChat/FileUtils.swift#L260). +- Timed message expiry triggers file deletion +- [`deleteAppDatabaseAndFiles()`](../SimpleXChat/FileUtils.swift#L86) removes all databases, files, temp files, and wallpapers +- [`deleteAppFiles()`](../SimpleXChat/FileUtils.swift#L108) removes only the files directory (preserving databases) + +--- + +## 7. Export & Import + +### Export + +```swift +ChatCommand.apiExportArchive(config: ArchiveConfig) +// Response: ChatResponse2.archiveExported(archiveErrors: [ArchiveError]) +``` + +`ArchiveConfig` specifies: +- `archivePath: String` -- destination path for the archive +- `disableCompression: Bool?` -- optional flag to skip compression + +The archive contains both databases and optionally files. The Haskell core handles the actual export, creating a ZIP archive. + +### Import + +```swift +ChatCommand.apiImportArchive(config: ArchiveConfig) +// Response: ChatResponse2.archiveImported(archiveErrors: [ArchiveError]) +``` + +Import replaces the current databases with the archive contents. The app must be restarted after import. + +### Archive Errors + +`ArchiveError` is an array returned with both export and import results, listing any non-fatal issues encountered (e.g., missing files, corrupt entries). + +--- + +## 8. App Group Sharing + +### Shared Access Model + +The main app and NSE share database access through the iOS App Group container: + +``` +Main App ──┐ + ├── {App Group}/simplex_v1_chat.db + ├── {App Group}/simplex_v1_agent.db +NSE ────────┘ +``` + +### Coordination + +- Both processes can initialize their own `chat_ctrl` instance pointing to the same database files +- SQLite WAL mode allows concurrent reads +- Write coordination uses `chat_close_store` / `chat_reopen_store` to manage database locks +- The main app suspends its chat controller when entering background, allowing NSE to access the database +- NSE is short-lived (~30 seconds per notification) and releases its lock quickly + +### App State Communication + +The `appStateGroupDefault` in `GroupDefaults` communicates app state between main app and NSE: +- `.active` -- main app is in foreground +- `.suspended` -- main app is in background +- `.stopped` -- main app is terminated + +The NSE checks this flag to determine whether to process notifications (it avoids processing if the main app is active). + +--- + +## Source Files + +| File | Path | +|------|------| +| File utilities & constants | [`SimpleXChat/FileUtils.swift`](../SimpleXChat/FileUtils.swift) | +| Database management UI | [`Shared/Views/Database/DatabaseView.swift`](../Shared/Views/Database/DatabaseView.swift) | +| Encryption settings UI | [`Shared/Views/Database/DatabaseEncryptionView.swift`](../Shared/Views/Database/DatabaseEncryptionView.swift) | +| C FFI (migration, file ops) | `SimpleXChat/SimpleX.h` | +| Haskell store root | `../../src/Simplex/Chat/Store/` | +| Haskell migrations | `../../src/Simplex/Chat/Store/SQLite/Migrations/` | diff --git a/apps/ios/spec/impact.md b/apps/ios/spec/impact.md new file mode 100644 index 0000000000..9593419b87 --- /dev/null +++ b/apps/ios/spec/impact.md @@ -0,0 +1,114 @@ +# SimpleX Chat iOS -- Impact Graph + +> Source file → product concept mapping. Use this to identify which product documents must be updated when a source file changes. +> +> Derived from [CODE.md](../CODE.md) Document Map and [product/concepts.md](../product/concepts.md). + +--- + +## Product Concept Legend + +| ID | Concept | +|----|---------| +| PC1 | Chat List | +| PC2 | Direct Chat | +| PC3 | Group Chat | +| PC4 | Message Composition | +| PC5 | Message Reactions | +| PC6 | Message Editing | +| PC7 | Message Deletion | +| PC8 | Timed Messages | +| PC9 | Voice Messages | +| PC10 | File Transfer | +| PC11 | Link Previews | +| PC12 | Contact Connection | +| PC13 | Contact Verification | +| PC14 | Group Management | +| PC15 | Group Links | +| PC16 | Member Roles | +| PC17 | Audio/Video Calls | +| PC18 | Push Notifications | +| PC19 | User Profiles | +| PC20 | Incognito Mode | +| PC21 | Hidden Profiles | +| PC22 | Local Authentication | +| PC23 | Database Encryption | +| PC24 | Theme System | +| PC25 | Network Configuration | +| PC26 | Device Migration | +| PC27 | Remote Desktop | +| PC28 | Chat Tags | +| PC29 | User Address | +| PC30 | Member Support Chat | + +--- + +## 1. Swift Source Impact + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| Shared/ContentView.swift | PC1, PC2, PC3 | High | Root navigation — affects all chat access | +| Shared/SimpleXApp.swift | PC1 through PC30 | High | App entry point — initialization affects everything | +| Shared/AppDelegate.swift | PC18 | Medium | Push notification registration | +| Shared/Views/ChatList/ChatListView.swift | PC1, PC28 | High | Main screen rendering and filtering | +| Shared/Views/Chat/ChatView.swift | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11 | High | Core conversation UI — most messaging features | +| Shared/Views/Chat/ComposeMessage/ComposeView.swift | PC4, PC6, PC9, PC11 | High | Message composition — send path for all messages | +| Shared/Views/Chat/ChatItem/ | PC2, PC3, PC5, PC7, PC8, PC9, PC10, PC11 | Medium | Individual message rendering components | +| Shared/Views/Chat/ChatInfoView.swift | PC2, PC13, PC20 | Medium | Contact details and verification | +| Shared/Views/Chat/Group/GroupChatInfoView.swift | PC3, PC14, PC15, PC16, PC30 | High | Group management hub | +| Shared/Views/Chat/Group/AddGroupMembersView.swift | PC14, PC16 | Medium | Member invitation flow | +| Shared/Views/Chat/Group/GroupLinkView.swift | PC15 | Low | Group link creation/sharing | +| Shared/Views/Chat/Group/GroupMemberInfoView.swift | PC3, PC14, PC16, PC30 | Medium | Member details and role management | +| Shared/Views/NewChat/NewChatView.swift | PC12 | High | New connection creation — onramp for all contacts | +| Shared/Views/NewChat/QRCode.swift | PC12 | Low | QR code display/scanning utility | +| Shared/Views/Call/ActiveCallView.swift | PC17 | Medium | Call UI rendering | +| Shared/Views/Call/CallController.swift | PC17 | High | CallKit integration — call lifecycle | +| Shared/Views/Call/WebRTCClient.swift | PC17 | High | WebRTC session management | +| Shared/Views/UserSettings/SettingsView.swift | PC18, PC22, PC23, PC24, PC25, PC29 | Medium | Settings navigation hub | +| Shared/Views/UserSettings/AppearanceSettings.swift | PC24 | Low | Theme customization UI | +| Shared/Views/UserSettings/NetworkAndServers/ | PC25 | High | Server configuration — affects connectivity | +| Shared/Views/UserSettings/UserProfilesView.swift | PC19, PC21 | Medium | Profile management | +| Shared/Views/Onboarding/ | PC1 | Medium | First-time setup — affects initial state | +| Shared/Views/LocalAuth/ | PC22 | Medium | App lock functionality | +| Shared/Views/Database/ | PC23, PC26 | High | Database encryption and export | +| Shared/Views/Migration/ | PC26 | High | Device migration — data portability | +| Shared/Model/ChatModel.swift | PC1 through PC30 | High | Central state — all features depend on it | +| Shared/Model/SimpleXAPI.swift | PC1 through PC30 | High | FFI bridge — all commands flow through here | +| Shared/Model/AppAPITypes.swift | PC1 through PC30 | High | Command/response types — all API communication | +| Shared/Model/NtfManager.swift | PC18 | High | Notification delivery | +| Shared/Model/BGManager.swift | PC18 | Medium | Background fetch scheduling | +| Shared/Theme/ThemeManager.swift | PC24 | Medium | Theme resolution engine | +| SimpleXChat/ChatTypes.swift | PC1 through PC30 | High | Core data types — all features use them | +| SimpleXChat/APITypes.swift | PC1 through PC30 | High | API result types and error handling | +| SimpleXChat/CallTypes.swift | PC17 | Medium | Call-specific data types | +| SimpleXChat/FileUtils.swift | PC10, PC23, PC26 | Medium | File paths and encryption utilities | +| SimpleXChat/Notifications.swift | PC18 | Medium | Notification type definitions | +| SimpleX NSE/NotificationService.swift | PC18 | High | Push notification decryption and display | + +--- + +## 2. Haskell Core Impact + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| src/Simplex/Chat/Controller.hs | PC1 through PC30 | High | Command processor — all API commands | +| src/Simplex/Chat/Types.hs | PC1 through PC30 | High | Core data types shared across all features | +| src/Simplex/Chat/Core.hs | PC1 through PC30 | High | Chat engine lifecycle | +| src/Simplex/Chat/Protocol.hs | PC2, PC3, PC4, PC5, PC6, PC7 | High | Chat-level message protocol (x-events) | +| src/Simplex/Chat/Messages.hs | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9 | High | Message types and content | +| src/Simplex/Chat/Messages/CIContent.hs | PC4, PC5, PC6, PC7, PC8, PC9, PC11 | Medium | Chat item content variants | +| src/Simplex/Chat/Call.hs | PC17 | Medium | Call signaling types | +| src/Simplex/Chat/Files.hs | PC10 | Medium | File transfer orchestration | +| src/Simplex/Chat/Store/Messages.hs | PC4, PC5, PC6, PC7, PC8 | High | Message persistence | +| src/Simplex/Chat/Store/Groups.hs | PC3, PC14, PC15, PC16, PC30 | High | Group persistence | +| src/Simplex/Chat/Store/Direct.hs | PC2, PC12, PC13 | High | Contact persistence | +| src/Simplex/Chat/Store/Files.hs | PC10 | Medium | File transfer persistence | +| src/Simplex/Chat/Store/Profiles.hs | PC19, PC21 | Medium | User profile persistence | +| src/Simplex/Chat/Store/Connections.hs | PC2, PC12 | High | Connection persistence and entity resolution | +| src/Simplex/Chat/Archive.hs | PC26 | Medium | Database export/import for migration | +| src/Simplex/Chat/ProfileGenerator.hs | PC20 | Low | Random profile generation for incognito | +| src/Simplex/Chat/Remote.hs | PC27 | Medium | Remote desktop protocol handler | +| src/Simplex/Chat/Remote/Types.hs | PC27 | Low | Remote desktop data types | +| src/Simplex/Chat/Types/UITheme.hs | PC24 | Low | Theme data types for UI customization | +| src/Simplex/Chat/Types/Preferences.hs | PC2, PC3, PC8 | Medium | Chat feature preferences (timed messages, etc.) | +| src/Simplex/Chat/Types/Shared.hs | PC3, PC16 | Medium | Shared types including GroupMemberRole | diff --git a/apps/ios/spec/services/calls.md b/apps/ios/spec/services/calls.md new file mode 100644 index 0000000000..6a1d89f6a3 --- /dev/null +++ b/apps/ios/spec/services/calls.md @@ -0,0 +1,383 @@ +# SimpleX Chat iOS -- WebRTC Calling Service + +> Technical specification for the calling system: CallController, WebRTCClient, CallKit integration, and signaling via SMP. +> +> Related specs: [Architecture](../architecture.md) | [API Reference](../api.md) | [Notifications](notifications.md) | [README](../README.md) +> Related product: [Chat View](../../product/views/chat.md) + +**Source:** [`CallController.swift`](../../Shared/Views/Call/CallController.swift) | [`WebRTCClient.swift`](../../Shared/Views/Call/WebRTCClient.swift) | [`ActiveCallView.swift`](../../Shared/Views/Call/ActiveCallView.swift) | [`CallTypes.swift`](../../SimpleXChat/CallTypes.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [CallController](#2-callcontroller) +3. [WebRTCClient](#3-webrtcclient) +4. [Call Flow via SMP](#4-call-flow-via-smp) +5. [CallKit Integration](#5-callkit-integration) +6. [CallKit-Free Mode](#6-callkit-free-mode) +7. [Audio Routing](#7-audio-routing) +8. [Key Types](#8-key-types) +9. [ActiveCallView](#9-activecallview) + +--- + +## 1. Overview + +SimpleX Chat provides end-to-end encrypted audio and video calls using WebRTC. The unique aspect is that all call signaling (SDP offers/answers, ICE candidates) is transmitted through the same encrypted SMP messaging channels used for chat, eliminating the need for a separate signaling server. + +``` +Caller SMP Relay Callee + │ │ │ + ├─ apiSendCallInvitation ──────→│──── push/event ──────→│ + │ │ │ + │ │←── apiSendCallOffer ──┤ + │←── ChatEvent.callOffer ───────│ │ + │ │ │ + ├─ apiSendCallAnswer ──────────→│──── callAnswer ──────→│ + │ │ │ + │←── callExtraInfo (ICE) ───────│←── apiSendCallExtraInfo│ + ├─ apiSendCallExtraInfo ───────→│──── callExtraInfo ───→│ + │ │ │ + │◄══════════ WebRTC P2P Media Stream ═══════════════════►│ + │ │ │ + ├─ apiEndCall ─────────────────→│──── callEnded ───────→│ +``` + +--- + +## [2. CallController](../../Shared/Views/Call/CallController.swift#L19-L449) + +**File**: `Shared/Views/Call/CallController.swift` + +Central call coordinator that bridges SimpleX call protocol with iOS CallKit (or non-CallKit fallback). + +### [Class Definition](../../Shared/Views/Call/CallController.swift#L19-L48) + +```swift +class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, ObservableObject { + static let shared = CallController() + static let isInChina = SKStorefront().countryCode == "CHN" + static func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } + + private let provider: CXProvider // CallKit provider + private let controller: CXCallController // CallKit controller + private let callManager: CallManager // Internal call state + private let registry: PKPushRegistry // VoIP push registration + + @Published var activeCallInvitation: RcvCallInvitation? + var shouldSuspendChat: Bool = false + var fulfillOnConnect: CXAnswerCallAction? = nil +} +``` + +### Key Responsibilities + +| Method | Purpose | Line | +|--------|---------|------| +| [`reportNewIncomingCall()`](../../Shared/Views/Call/CallController.swift#L287) | Reports incoming call to CallKit for native UI | L287 | +| [`reportOutgoingCall()`](../../Shared/Views/Call/CallController.swift#L328) | Reports outgoing call to CallKit | L328 | +| [`provider(_:perform: CXAnswerCallAction)`](../../Shared/Views/Call/CallController.swift#L66) | Handles user answering via CallKit UI | L66 | +| [`provider(_:perform: CXEndCallAction)`](../../Shared/Views/Call/CallController.swift#L96) | Handles user ending via CallKit UI | L96 | +| [`provider(_:perform: CXStartCallAction)`](../../Shared/Views/Call/CallController.swift#L55) | Handles outgoing call start | L55 | +| [`pushRegistry(_:didReceiveIncomingPushWith:)`](../../Shared/Views/Call/CallController.swift#L202) | Handles VoIP push tokens | L202 | +| [`hasActiveCalls()`](../../Shared/Views/Call/CallController.swift#L435) | Checks if any calls are active | L435 | + +### Call Manager (internal) + +`CallManager` tracks call state internally: +- Maps call UUIDs to `Call` objects +- Handles call state transitions +- Coordinates between CallKit actions and SimpleX API calls + +--- + +## [3. WebRTCClient](../../Shared/Views/Call/WebRTCClient.swift#L13-L676) + +**File**: `Shared/Views/Call/WebRTCClient.swift` (~49KB) + +Manages the WebRTC peer connection, media streams, and data channels. + +### Responsibilities + +- Creates and configures `RTCPeerConnection` +- Manages local audio/video capture (`RTCCameraVideoCapturer`, `RTCAudioTrack`) +- Handles SDP offer/answer creation and application +- Processes ICE candidates +- Manages media stream encryption + +### Key Operations + +| Operation | Description | Line | +|-----------|-------------|------| +| [`initializeCall`](../../Shared/Views/Call/WebRTCClient.swift#L93) | Sets up peer connection, tracks, encryption | L93 | +| [`createPeerConnection`](../../Shared/Views/Call/WebRTCClient.swift#L139) | Creates and configures RTCPeerConnection | L139 | +| [`sendCallCommand`](../../Shared/Views/Call/WebRTCClient.swift#L176) | Dispatches WCallCommand (offer/answer/ICE) | L176 | +| [`addIceCandidates`](../../Shared/Views/Call/WebRTCClient.swift#L165) | `peerConnection.add(RTCIceCandidate)` | L165 | +| [`getInitialIceCandidates`](../../Shared/Views/Call/WebRTCClient.swift#L285) | Collects initial ICE candidates | L285 | +| [`sendIceCandidates`](../../Shared/Views/Call/WebRTCClient.swift#L305) | Sends gathered ICE candidates | L305 | +| [`enableMedia`](../../Shared/Views/Call/WebRTCClient.swift#L365) | Enable/disable audio or video track | L365 | +| [`setupLocalTracks`](../../Shared/Views/Call/WebRTCClient.swift#L423) | Creates audio/video tracks and adds to connection | L423 | +| [`startCaptureLocalVideo`](../../Shared/Views/Call/WebRTCClient.swift#L581) | Front/back camera toggle and capture start | L581 | +| [`endCall`](../../Shared/Views/Call/WebRTCClient.swift#L645) | Tears down connection and tracks | L645 | +| [`setupEncryptionForLocalTracks`](../../Shared/Views/Call/WebRTCClient.swift#L503) | Sets up frame encryption for local media tracks | L503 | + +### [Additional Encryption](../../Shared/Views/Call/WebRTCClient.swift#L513-L546) + +Beyond WebRTC's built-in SRTP encryption, SimpleX adds an extra encryption layer: +- A shared key from the E2E SMP channel is used +- Applied via `chat_encrypt_media` / `chat_decrypt_media` C FFI functions +- Each media frame is encrypted/decrypted with this additional key +- Provides defense-in-depth even if SRTP is compromised + +--- + +## 4. Call Flow via SMP + +All call signaling travels through the same encrypted SMP message channels used for chat. No separate signaling server is needed. + +### Outgoing Call (Caller Side) + +``` +1. User initiates call + └── apiSendCallInvitation(contact:, callType:) + └── Sends CallInvitation via SMP to contact + +2. Callee accepts, sends SDP offer + └── ChatEvent.callOffer received + └── WebRTCClient creates answer + └── apiSendCallAnswer(contact:, answer:) + +3. ICE candidates exchanged + └── ChatEvent.callExtraInfo received → WebRTCClient.addIceCandidate() + └── WebRTCClient generates candidates → apiSendCallExtraInfo(contact:, extraInfo:) + +4. P2P connection established + └── Media streams flowing + +5. End call + └── apiEndCall(contact:) +``` + +### Incoming Call (Callee Side) + +``` +1. ChatEvent.callInvitation received (or push notification) + └── CallController reports to CallKit (or shows in-app notification) + +2. User accepts + └── WebRTCClient creates SDP offer (callee creates offer in SimpleX protocol) + └── apiSendCallOffer(contact:, callOffer:) + +3. Caller sends answer + └── ChatEvent.callAnswer received + └── WebRTCClient.setRemoteDescription(answer) + +4. ICE candidates exchanged (same as above) + +5. P2P connection established +``` + +### API Commands + +| Command | Direction | Purpose | +|---------|-----------|---------| +| `apiSendCallInvitation(contact:, callType:)` | Caller -> Callee | Initiate call | +| `apiRejectCall(contact:)` | Callee -> Caller | Reject call | +| `apiSendCallOffer(contact:, callOffer:)` | Callee -> Caller | Send SDP offer | +| `apiSendCallAnswer(contact:, answer:)` | Caller -> Callee | Send SDP answer | +| `apiSendCallExtraInfo(contact:, extraInfo:)` | Both | Send ICE candidates | +| `apiEndCall(contact:)` | Either | End call | +| `apiGetCallInvitations` | -- | Get pending invitations | +| `apiCallStatus(contact:, callStatus:)` | -- | Report status change | + +--- + +## [5. CallKit Integration](../../Shared/Views/Call/CallController.swift#L24-L155) + +CallKit provides the native iOS incoming call experience (lock screen UI, call history, system call handling). + +### [CXProvider Configuration](../../Shared/Views/Call/CallController.swift#L24-L37) + +```swift +let configuration = CXProviderConfiguration() +configuration.supportsVideo = true +configuration.supportedHandleTypes = [.generic] +configuration.includesCallsInRecents = UserDefaults.standard.bool( + forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS +) +configuration.maximumCallGroups = 1 +configuration.maximumCallsPerCallGroup = 1 +configuration.iconTemplateImageData = UIImage(named: "icon-transparent")?.pngData() +``` + +### [VoIP Push (PKPushRegistry)](../../Shared/Views/Call/CallController.swift#L207-L284) + +CallKit requires VoIP push for incoming calls on locked device: +- `PKPushRegistry` registers for `.voIP` push type +- VoIP push token is separate from regular APNs token +- When VoIP push received, **must** report an incoming call to CallKit within the callback (iOS requirement) + +### CallKit Actions + +| CXAction | Handler | Description | Line | +|----------|---------|-------------|------| +| `CXStartCallAction` | [`provider(_:perform:)`](../../Shared/Views/Call/CallController.swift#L55) | User starts outgoing call | L55 | +| `CXAnswerCallAction` | [`provider(_:perform:)`](../../Shared/Views/Call/CallController.swift#L66) | User answers incoming call from CallKit UI | L66 | +| `CXEndCallAction` | [`provider(_:perform:)`](../../Shared/Views/Call/CallController.swift#L96) | User ends call from CallKit UI | L96 | +| `CXSetMutedCallAction` | [`provider(_:perform:)`](../../Shared/Views/Call/CallController.swift#L112) | User mutes from CallKit UI | L112 | + +### [Lock Screen Answer](../../Shared/Views/Call/CallController.swift#L66-L94) + +When answering from the lock screen: +1. `CXAnswerCallAction` fires +2. CallController waits for chat to be ready ([`waitUntilChatStarted(timeoutMs: 30_000)`](../../Shared/Views/Call/CallController.swift#L183)) +3. WebRTC connection established +4. `fulfillOnConnect` action is fulfilled only when WebRTC reaches connected state (required for audio to work on lock screen) + +--- + +## [6. CallKit-Free Mode](../../Shared/Views/Call/CallController.swift#L21-L22) + +In regions where CallKit is unavailable (e.g., China, determined by `SKStorefront.countryCode == "CHN"`), the app falls back to in-app notifications: + +```swift +static let isInChina = SKStorefront().countryCode == "CHN" +static func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } +``` + +### Non-CallKit Behavior +- Incoming calls shown as in-app banners (via `CallController.activeCallInvitation`) +- No lock screen call UI +- No system call integration +- User can also manually disable CallKit via settings (`callKitEnabledGroupDefault`) + +--- + +## [7. Audio Routing](../../Shared/Views/Call/WebRTCClient.swift#L907-L1005) + +### [AVAudioSession Management](../../Shared/Views/Call/WebRTCClient.swift#L907-L950) + +Audio routing is managed through `AVAudioSession`: +- **Receiver**: Default for audio-only calls (ear speaker) +- **Speaker**: For video calls or when user toggles speaker +- **Bluetooth**: Detected and used when available +- **Headphones**: Detected and used when connected + +### Route Change Handling + +The `WebRTCClient` observes `AVAudioSession.routeChangeNotification` to handle: +- Bluetooth device connection/disconnection +- Headphone plug/unplug +- Speaker/receiver toggle + +--- + +## [8. Key Types](../../SimpleXChat/CallTypes.swift#L1-L115) + +### [RcvCallInvitation](../../SimpleXChat/CallTypes.swift#L45-L71) + +```swift +struct RcvCallInvitation { + var user: User + var contact: Contact + var callType: CallType + var sharedKey: String? // Optional E2E encryption key + var callUUID: String? + var callTs: Date +} +``` + +### [CallType](../../SimpleXChat/CallTypes.swift#L74-L82) + +```swift +struct CallType { + var media: CallMediaType // .audio or .video + var capabilities: CallCapabilities +} + +enum CallMediaType: String { + case audio + case video +} +``` + +### [WebRTCCallOffer](../../SimpleXChat/CallTypes.swift#L14-L22) / [WebRTCSession](../../SimpleXChat/CallTypes.swift#L25-L33) + +```swift +struct WebRTCCallOffer { + var callType: CallType + var rtcSession: WebRTCSession +} + +struct WebRTCSession { + var rtcSession: String // SDP string + var rtcIceCandidates: String // ICE candidates JSON +} +``` + +### [WebRTCExtraInfo](../../SimpleXChat/CallTypes.swift#L36-L42) + +```swift +struct WebRTCExtraInfo { + var rtcIceCandidates: String // Additional ICE candidates +} +``` + +### Call (Active Call State) + +Stored in `ChatModel.activeCall`: +- Contact reference +- Call UUID +- Call state (enum: `.waitCapabilities`, `.invitationAccepted`, `.offerSent`, `.answerReceived`, `.connected`, etc.) +- Media type +- WebRTCClient reference + +--- + +## [9. ActiveCallView](../../Shared/Views/Call/ActiveCallView.swift#L16-L285) + +**File**: `Shared/Views/Call/ActiveCallView.swift` + +Full-screen call UI when `ChatModel.showCallView == true`: + +### UI Elements +- Remote video (full screen background) +- Local video (PiP corner, draggable) +- Contact name and call duration +- Control buttons: mute, camera toggle, speaker toggle, camera flip, end call +- Minimize button (collapses to banner) + +### [ActiveCallOverlay](../../Shared/Views/Call/ActiveCallView.swift#L288-L522) + +| Control | Method | Line | +|---------|--------|------| +| Audio call info | [`audioCallInfoView`](../../Shared/Views/Call/ActiveCallView.swift#L357) | L357 | +| Video call info | [`videoCallInfoView`](../../Shared/Views/Call/ActiveCallView.swift#L377) | L377 | +| End call | [`endCallButton`](../../Shared/Views/Call/ActiveCallView.swift#L407) | L407 | +| Mute toggle | [`toggleMicButton`](../../Shared/Views/Call/ActiveCallView.swift#L418) | L418 | +| Audio device | [`audioDeviceButton`](../../Shared/Views/Call/ActiveCallView.swift#L428) | L428 | +| Speaker toggle | [`toggleSpeakerButton`](../../Shared/Views/Call/ActiveCallView.swift#L452) | L452 | +| Camera toggle | [`toggleCameraButton`](../../Shared/Views/Call/ActiveCallView.swift#L464) | L464 | +| Flip camera | [`flipCameraButton`](../../Shared/Views/Call/ActiveCallView.swift#L475) | L475 | + +### PiP (Picture-in-Picture) + +When `ChatModel.activeCallViewIsCollapsed == true`: +- Call view collapses to a small floating overlay +- User can return to full-screen by tapping the banner +- Navigation continues normally underneath + +--- + +## Source Files + +| File | Path | Lines | +|------|------|-------| +| [Call controller](../../Shared/Views/Call/CallController.swift) | `Shared/Views/Call/CallController.swift` | 449 | +| [WebRTC client](../../Shared/Views/Call/WebRTCClient.swift) | `Shared/Views/Call/WebRTCClient.swift` | 1139 | +| [Active call UI](../../Shared/Views/Call/ActiveCallView.swift) | `Shared/Views/Call/ActiveCallView.swift` | 528 | +| WebRTC helpers | `Shared/Views/Call/WebRTC.swift` | | +| [Call types (Swift)](../../SimpleXChat/CallTypes.swift) | `SimpleXChat/CallTypes.swift` | 115 | +| Call types (Haskell) | `../../src/Simplex/Chat/Call.hs` | | diff --git a/apps/ios/spec/services/files.md b/apps/ios/spec/services/files.md new file mode 100644 index 0000000000..7e1f8a2ad1 --- /dev/null +++ b/apps/ios/spec/services/files.md @@ -0,0 +1,368 @@ +# SimpleX Chat iOS -- File Transfer Service + +> Technical specification for file transfer: inline/XFTP protocols, auto-receive thresholds, CryptoFile encryption, and file constants. +> +> Related specs: [Compose Module](../client/compose.md) | [Chat View](../client/chat-view.md) | [API Reference](../api.md) | [Database](../database.md) | [README](../README.md) +> Related product: [Product Overview](../../product/README.md) + +**Source:** [`FileUtils.swift`](../../SimpleXChat/FileUtils.swift) | [`CryptoFile.swift`](../../SimpleXChat/CryptoFile.swift) | [`ChatTypes.swift`](../../SimpleXChat/ChatTypes.swift) | [`AppAPITypes.swift`](../../Shared/Model/AppAPITypes.swift) | [`SimpleXAPI.swift`](../../Shared/Model/SimpleXAPI.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Transfer Methods](#2-transfer-methods) +3. [Auto-Receive Thresholds](#3-auto-receive-thresholds) +4. [File Size Constants](#4-file-size-constants) +5. [Image Handling](#5-image-handling) +6. [Voice Messages](#6-voice-messages) +7. [CryptoFile -- At-Rest Encryption](#7-cryptofile) +8. [File Storage Paths](#8-file-storage-paths) +9. [File Lifecycle](#9-file-lifecycle) +10. [API Commands](#10-api-commands) + +--- + +## 1. Overview + +SimpleX Chat supports two file transfer methods depending on file size: + +``` +File ≤ 255KB (inline) +├── Base64 encoded directly in SMP message +├── Single message delivery +└── No extra server infrastructure needed + +File > 255KB up to 1GB (XFTP) +├── Encrypted and chunked +├── Uploaded to XFTP relay servers +├── Recipient downloads chunks from relays +└── Files auto-deleted from relays after download or expiry +``` + +All files are end-to-end encrypted. The XFTP protocol adds a second encryption layer on top of the SMP channel encryption. + +--- + +## 2. Transfer Methods + +### Inline Transfer + +- Files up to [`MAX_IMAGE_SIZE`](../../SimpleXChat/FileUtils.swift#L18) (255KB) are base64-encoded and embedded directly in the SMP message body +- No additional protocol or server needed +- Delivered with the same reliability guarantees as regular messages +- Used primarily for compressed images + +### XFTP Transfer + +For files exceeding the inline threshold (up to [`MAX_FILE_SIZE_XFTP`](../../SimpleXChat/FileUtils.swift#L30) = 1GB): + +1. **Sender side**: + - File is AES-encrypted with a random key + - Encrypted file is split into chunks + - Chunks are uploaded to one or more XFTP relay servers + - File metadata (key, chunk locations) sent to recipient via SMP message + +2. **Recipient side**: + - Receives file metadata via SMP + - Downloads chunks from XFTP relays + - Reassembles and decrypts the file + +3. **Cleanup**: + - XFTP relays delete chunks after download or after expiry period + - No persistent storage on relays + +### SMP Transfer (legacy) + +[`MAX_FILE_SIZE_SMP`](../../SimpleXChat/FileUtils.swift#L34) (8MB) exists as a constant for larger inline transfers through SMP, used in specific scenarios. + +--- + +## 3. Auto-Receive Thresholds + +Files below certain size thresholds are automatically accepted and downloaded without user confirmation: + +| Media Type | Auto-Receive Threshold | Constant | Line | +|------------|----------------------|----------|------| +| Images | 510 KB | [`MAX_IMAGE_SIZE_AUTO_RCV`](../../SimpleXChat/FileUtils.swift#L21) | [L21](../../SimpleXChat/FileUtils.swift#L21) | +| Voice messages | 510 KB | [`MAX_VOICE_SIZE_AUTO_RCV`](../../SimpleXChat/FileUtils.swift#L24) | [L24](../../SimpleXChat/FileUtils.swift#L24) | +| Video | 1023 KB | [`MAX_VIDEO_SIZE_AUTO_RCV`](../../SimpleXChat/FileUtils.swift#L27) | [L27](../../SimpleXChat/FileUtils.swift#L27) | +| Other files | Not auto-received | Requires manual acceptance | -- | + +### Behavior + +- When a message with a file attachment arrives: + 1. Check if file size is below the auto-receive threshold for its type + 2. If below: automatically call [`setFileToReceive(fileId:, userApprovedRelays:, encrypted:)`](../../Shared/Model/AppAPITypes.swift#L168) followed by download + 3. If above: show download button in chat item, wait for user action + 4. User manually triggers download via [`receiveFile(fileId:, userApprovedRelays:, encrypted:, inline:)`](../../Shared/Model/AppAPITypes.swift#L167) + +### Relay Approval + +`userApprovedRelays` parameter: when the file is hosted on relays not in the user's configured server list, the user is asked for confirmation before connecting to unknown relays. + +--- + +## [4. File Size Constants](../../SimpleXChat/FileUtils.swift#L18) + +Defined in [`SimpleXChat/FileUtils.swift`](../../SimpleXChat/FileUtils.swift): + +| Constant | Value | Line | +|----------|-------|------| +| `MAX_IMAGE_SIZE` | 261,120 (255 KB) | [L18](../../SimpleXChat/FileUtils.swift#L18) | +| `MAX_IMAGE_SIZE_AUTO_RCV` | 522,240 (510 KB) | [L21](../../SimpleXChat/FileUtils.swift#L21) | +| `MAX_VOICE_SIZE_AUTO_RCV` | 522,240 (510 KB) | [L24](../../SimpleXChat/FileUtils.swift#L24) | +| `MAX_VIDEO_SIZE_AUTO_RCV` | 1,047,552 (1023 KB) | [L27](../../SimpleXChat/FileUtils.swift#L27) | +| `MAX_FILE_SIZE_XFTP` | 1,073,741,824 (1 GB) | [L30](../../SimpleXChat/FileUtils.swift#L30) | +| `MAX_FILE_SIZE_LOCAL` | Int64.max (no limit) | [L32](../../SimpleXChat/FileUtils.swift#L32) | +| `MAX_FILE_SIZE_SMP` | 8,000,000 (~7.6 MB) | [L34](../../SimpleXChat/FileUtils.swift#L34) | +| `MAX_VOICE_MESSAGE_LENGTH` | 300 s (5 min) | [L36](../../SimpleXChat/FileUtils.swift#L36) | + +```swift +// Image compression target for inline transfer +public let MAX_IMAGE_SIZE: Int64 = 261_120 // 255 KB + +// Auto-receive thresholds +public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = 522_240 // 510 KB (2 * MAX_IMAGE_SIZE) +public let MAX_VOICE_SIZE_AUTO_RCV: Int64 = 522_240 // 510 KB (2 * MAX_IMAGE_SIZE) +public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023 KB + +// Transfer method limits +public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1 GB +public let MAX_FILE_SIZE_SMP: Int64 = 8_000_000 // ~7.6 MB +public let MAX_FILE_SIZE_LOCAL: Int64 = Int64.max // No limit (local notes) + +// Voice message constraints +public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(300) // 5 minutes (300 seconds) +``` + +--- + +## 5. Image Handling + +### Compression Pipeline + +1. User selects image (camera or photo library) +2. Image is compressed to fit within [`MAX_IMAGE_SIZE`](../../SimpleXChat/FileUtils.swift#L18) (255KB): + - Progressive JPEG compression with decreasing quality + - Resize if dimensions are too large +3. Compressed image is base64-encoded into the message content +4. For larger images that cannot compress to 255KB: sent via XFTP + +### Display + +- `CIImageView` renders images in chat bubbles with aspect-fit sizing +- Tapping opens `FullScreenMediaView` with zoom/pan/share capabilities +- Thumbnail is displayed immediately; full-size loaded on demand for XFTP images + +### Animated Images + +- GIFs are handled by `AnimatedImageView` +- Displayed inline with animation support + +--- + +## 6. Voice Messages + +### Recording + +1. `ComposeVoiceView` manages the recording UI +2. `AudioRecPlay` handles `AVAudioRecorder` lifecycle +3. Recorded in compressed audio format +4. Maximum duration: [`MAX_VOICE_MESSAGE_LENGTH`](../../SimpleXChat/FileUtils.swift#L36) = 300 seconds (5 minutes) +5. Waveform data extracted for visualization + +### Transfer + +- Voice files up to [`MAX_VOICE_SIZE_AUTO_RCV`](../../SimpleXChat/FileUtils.swift#L24) (510KB) are auto-received +- Larger voice files follow standard file transfer flow +- Voice messages include waveform metadata for UI rendering + +### Playback + +- `CIVoiceView` / `FramedCIVoiceView` render voice messages +- Shows waveform visualization and play/pause control +- `ChatModel.stopPreviousRecPlay` ensures only one audio source plays at a time +- Playback position and progress tracked + +--- + +## [7. CryptoFile -- At-Rest Encryption](../../SimpleXChat/ChatTypes.swift#L4241) + +When [`apiSetEncryptLocalFiles(enable: true)`](../../Shared/Model/SimpleXAPI.swift#L384) is configured, files stored on the device are AES-encrypted. + +### [`CryptoFile`](../../SimpleXChat/ChatTypes.swift#L4241) Type + +```swift +struct CryptoFile { + var filePath: String + var cryptoArgs: CryptoFileArgs? // nil = unencrypted +} + +struct CryptoFileArgs { + var fileKey: String // AES encryption key + var fileNonce: String // AES nonce/IV +} +``` + +> Defined in [`ChatTypes.swift` L4241](../../SimpleXChat/ChatTypes.swift#L4241) (`CryptoFile`) and [L4289](../../SimpleXChat/ChatTypes.swift#L4289) (`CryptoFileArgs`). + +### Encryption Operations (C FFI) + +Implemented in [`CryptoFile.swift`](../../SimpleXChat/CryptoFile.swift): + +| Function | Purpose | Line | +|----------|---------|------| +| [`writeCryptoFile`](../../SimpleXChat/CryptoFile.swift#L18) | Write encrypted file, returns `CryptoFileArgs` | [L18](../../SimpleXChat/CryptoFile.swift#L18) | +| [`readCryptoFile`](../../SimpleXChat/CryptoFile.swift#L31) | Read and decrypt file, returns `Data` | [L31](../../SimpleXChat/CryptoFile.swift#L31) | +| [`encryptCryptoFile`](../../SimpleXChat/CryptoFile.swift#L54) | Encrypt existing file to new path | [L54](../../SimpleXChat/CryptoFile.swift#L54) | +| [`decryptCryptoFile`](../../SimpleXChat/CryptoFile.swift#L66) | Decrypt file to new path | [L66](../../SimpleXChat/CryptoFile.swift#L66) | + +### Storage + +- Encrypted files stored alongside unencrypted files in `Documents/files/` +- The `CryptoFileArgs` (key + nonce) are stored in the Haskell database, not on the filesystem +- Toggle via privacy settings: [`apiSetEncryptLocalFiles(enable:)`](../../Shared/Model/SimpleXAPI.swift#L384) + +--- + +## [8. File Storage Paths](../../SimpleXChat/FileUtils.swift#L199) + +### Directory Structure + +| Function | Path | Line | +|----------|------|------| +| [`getAppFilesDirectory()`](../../SimpleXChat/FileUtils.swift#L208) | `Documents/files/` | [L208](../../SimpleXChat/FileUtils.swift#L208) | +| [`getTempFilesDirectory()`](../../SimpleXChat/FileUtils.swift#L199) | `Documents/temp_files/` | [L199](../../SimpleXChat/FileUtils.swift#L199) | +| [`getWallpaperDirectory()`](../../SimpleXChat/FileUtils.swift#L217) | `Documents/wallpapers/` | [L217](../../SimpleXChat/FileUtils.swift#L217) | +| [`getAppFilePath(_:)`](../../SimpleXChat/FileUtils.swift#L212) | `Documents/files/{filename}` | [L212](../../SimpleXChat/FileUtils.swift#L212) | +| [`getWallpaperFilePath(_:)`](../../SimpleXChat/FileUtils.swift#L221) | `Documents/wallpapers/{filename}` | [L221](../../SimpleXChat/FileUtils.swift#L221) | + +```swift +func getAppFilesDirectory() -> URL // Documents/files/ +func getTempFilesDirectory() -> URL // Documents/temp_files/ +func getWallpaperDirectory() -> URL // Documents/wallpapers/ +``` + +### Path Management + +- Downloaded files: `Documents/files/{filename}` +- Temporary files during transfer: `Documents/temp_files/` +- Wallpaper images: `Documents/wallpapers/` +- File paths are set via [`apiSetAppFilePaths(filesFolder:, tempFolder:, assetsFolder:)`](../../Shared/Model/SimpleXAPI.swift#L377) at startup + +--- + +## 9. File Lifecycle + +### Sending + +``` +1. User selects file/image/video in compose +2. ComposeView creates ComposedMessage with file reference +3. apiSendMessages() → Haskell core processes: + a. File ≤ inline threshold: base64 encode into message + b. File > inline threshold: start XFTP upload +4. Upload events: + - ChatEvent.sndFileStart + - ChatEvent.sndFileProgressXFTP (periodic progress) + - ChatEvent.sndFileCompleteXFTP (upload done) + - ChatEvent.sndFileError (on failure) +``` + +### Receiving + +``` +1. Message with file attachment arrives +2. Auto-receive check: + a. Below threshold: automatic download starts + b. Above threshold: user sees download button +3. User triggers download (or auto-triggered): + - receiveFile(fileId:, userApprovedRelays:, encrypted:, inline:) +4. Download events: + - ChatEvent.rcvFileStart + - ChatEvent.rcvFileProgressXFTP (periodic progress) + - ChatEvent.rcvFileComplete (download done) + - ChatEvent.rcvFileError (on failure) + - ChatEvent.rcvFileSndCancelled (sender cancelled) +``` + +### Cancellation + +```swift +ChatCommand.cancelFile(fileId: Int64) +``` + +Cancels an in-progress upload or download. For XFTP transfers, also requests chunk deletion from relays. + +### Cleanup + +| Function | Purpose | Line | +|----------|---------|------| +| [`cleanupFile(_:)`](../../SimpleXChat/FileUtils.swift#L267) | Remove file associated with a chat item | [L267](../../SimpleXChat/FileUtils.swift#L267) | +| [`cleanupDirectFile(_:)`](../../SimpleXChat/FileUtils.swift#L260) | Remove file only for direct chats | [L260](../../SimpleXChat/FileUtils.swift#L260) | +| [`removeFile(_:)`](../../SimpleXChat/FileUtils.swift#L243) | Delete file at URL | [L243](../../SimpleXChat/FileUtils.swift#L243) | +| [`removeFile(_:)`](../../SimpleXChat/FileUtils.swift#L251) | Delete file by name | [L251](../../SimpleXChat/FileUtils.swift#L251) | +| [`deleteAppFiles()`](../../SimpleXChat/FileUtils.swift#L108) | Remove all app files (preserving databases) | [L108](../../SimpleXChat/FileUtils.swift#L108) | +| [`deleteAppDatabaseAndFiles()`](../../SimpleXChat/FileUtils.swift#L86) | Remove everything | [L86](../../SimpleXChat/FileUtils.swift#L86) | + +- When a `ChatItem` is deleted, its associated file is deleted from disk +- When a timed message expires, its file is deleted +- `ChatModel.filesToDelete` queues files for deferred deletion +- [`deleteAppFiles()`](../../SimpleXChat/FileUtils.swift#L108) removes all files (preserving databases) +- [`deleteAppDatabaseAndFiles()`](../../SimpleXChat/FileUtils.swift#L86) removes everything + +--- + +## [10. API Commands](../../Shared/Model/AppAPITypes.swift#L167) + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| [`receiveFile`](../../Shared/Model/AppAPITypes.swift#L167) | `fileId, userApprovedRelays, encrypted, inline` | Accept and start downloading a file | [L167](../../Shared/Model/AppAPITypes.swift#L167) | +| [`setFileToReceive`](../../Shared/Model/AppAPITypes.swift#L168) | `fileId, userApprovedRelays, encrypted` | Mark file for auto-receive (no immediate download) | [L168](../../Shared/Model/AppAPITypes.swift#L168) | +| [`cancelFile`](../../Shared/Model/AppAPITypes.swift#L169) | `fileId` | Cancel in-progress transfer | [L169](../../Shared/Model/AppAPITypes.swift#L169) | +| [`apiUploadStandaloneFile`](../../Shared/Model/AppAPITypes.swift#L179) | `userId, file: CryptoFile` | Upload file to XFTP without a chat context | [L179](../../Shared/Model/AppAPITypes.swift#L179) | +| [`apiDownloadStandaloneFile`](../../Shared/Model/AppAPITypes.swift#L180) | `userId, url, file: CryptoFile` | Download from XFTP URL | [L180](../../Shared/Model/AppAPITypes.swift#L180) | +| [`apiStandaloneFileInfo`](../../Shared/Model/AppAPITypes.swift#L181) | `url` | Get metadata for an XFTP URL | [L181](../../Shared/Model/AppAPITypes.swift#L181) | + +### File Transfer Events + +| Event | Description | Line | +|-------|-------------|------| +| [`rcvFileAccepted`](../../Shared/Model/AppAPITypes.swift#L1095) | Download request accepted | [L1095](../../Shared/Model/AppAPITypes.swift#L1095) | +| [`rcvFileStart`](../../Shared/Model/AppAPITypes.swift#L1097) | Download started | [L1097](../../Shared/Model/AppAPITypes.swift#L1097) | +| [`rcvFileProgressXFTP`](../../Shared/Model/AppAPITypes.swift#L1098) | Download progress (receivedSize, totalSize) | [L1098](../../Shared/Model/AppAPITypes.swift#L1098) | +| [`rcvFileComplete`](../../Shared/Model/AppAPITypes.swift#L1099) | Download complete | [L1099](../../Shared/Model/AppAPITypes.swift#L1099) | +| [`rcvFileSndCancelled`](../../Shared/Model/AppAPITypes.swift#L1101) | Sender cancelled the transfer | [L1101](../../Shared/Model/AppAPITypes.swift#L1101) | +| [`rcvFileError`](../../Shared/Model/AppAPITypes.swift#L1102) | Download failed | [L1102](../../Shared/Model/AppAPITypes.swift#L1102) | +| [`rcvFileWarning`](../../Shared/Model/AppAPITypes.swift#L1103) | Download warning (non-fatal) | [L1103](../../Shared/Model/AppAPITypes.swift#L1103) | +| [`sndFileStart`](../../Shared/Model/AppAPITypes.swift#L1105) | Upload started | [L1105](../../Shared/Model/AppAPITypes.swift#L1105) | +| [`sndFileComplete`](../../Shared/Model/AppAPITypes.swift#L1106) | Inline upload complete | [L1106](../../Shared/Model/AppAPITypes.swift#L1106) | +| [`sndFileProgressXFTP`](../../Shared/Model/AppAPITypes.swift#L1108) | XFTP upload progress (sentSize, totalSize) | [L1108](../../Shared/Model/AppAPITypes.swift#L1108) | +| [`sndFileCompleteXFTP`](../../Shared/Model/AppAPITypes.swift#L1110) | XFTP upload complete | [L1110](../../Shared/Model/AppAPITypes.swift#L1110) | +| [`sndFileRcvCancelled`](../../Shared/Model/AppAPITypes.swift#L1107) | Receiver cancelled | [L1107](../../Shared/Model/AppAPITypes.swift#L1107) | +| [`sndFileError`](../../Shared/Model/AppAPITypes.swift#L1112) | Upload failed | [L1112](../../Shared/Model/AppAPITypes.swift#L1112) | +| [`sndFileWarning`](../../Shared/Model/AppAPITypes.swift#L1113) | Upload warning (non-fatal) | [L1113](../../Shared/Model/AppAPITypes.swift#L1113) | + +--- + +## Source Files + +| File | Path | Key Definitions | +|------|------|-----------------| +| File utilities & constants | [`SimpleXChat/FileUtils.swift`](../../SimpleXChat/FileUtils.swift) | `MAX_IMAGE_SIZE`, `saveFile`, `removeFile`, `getMaxFileSize` | +| CryptoFile FFI operations | [`SimpleXChat/CryptoFile.swift`](../../SimpleXChat/CryptoFile.swift) | `writeCryptoFile`, `readCryptoFile`, `encryptCryptoFile`, `decryptCryptoFile` | +| CryptoFile / CryptoFileArgs types | [`SimpleXChat/ChatTypes.swift`](../../SimpleXChat/ChatTypes.swift) | `CryptoFile` (L4241), `CryptoFileArgs` (L4289) | +| API command definitions | [`Shared/Model/AppAPITypes.swift`](../../Shared/Model/AppAPITypes.swift) | `receiveFile`, `cancelFile`, `ChatEvent` file events | +| API implementations | [`Shared/Model/SimpleXAPI.swift`](../../Shared/Model/SimpleXAPI.swift) | `receiveFile` (L1471), `cancelFile` (L1590) | +| File view (chat item) | [`Shared/Views/Chat/ChatItem/CIFileView.swift`](../../Shared/Views/Chat/ChatItem/CIFileView.swift) | | +| Image view (chat item) | [`Shared/Views/Chat/ChatItem/CIImageView.swift`](../../Shared/Views/Chat/ChatItem/CIImageView.swift) | | +| Video view (chat item) | [`Shared/Views/Chat/ChatItem/CIVideoView.swift`](../../Shared/Views/Chat/ChatItem/CIVideoView.swift) | | +| Voice view (chat item) | [`Shared/Views/Chat/ChatItem/CIVoiceView.swift`](../../Shared/Views/Chat/ChatItem/CIVoiceView.swift) | | +| Compose file preview | [`Shared/Views/Chat/ComposeMessage/ComposeFileView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeFileView.swift) | | +| Compose image preview | [`Shared/Views/Chat/ComposeMessage/ComposeImageView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeImageView.swift) | | +| Compose voice preview | [`Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift`](../../Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift) | | +| C FFI (file encryption) | [`SimpleXChat/SimpleX.h`](../../SimpleXChat/SimpleX.h) | `chat_write_file`, `chat_read_file`, `chat_encrypt_file`, `chat_decrypt_file` | +| Haskell file logic | `../../src/Simplex/Chat/Files.hs` | -- | +| Haskell file store | `../../src/Simplex/Chat/Store/Files.hs` | -- | diff --git a/apps/ios/spec/services/notifications.md b/apps/ios/spec/services/notifications.md new file mode 100644 index 0000000000..1062833f9c --- /dev/null +++ b/apps/ios/spec/services/notifications.md @@ -0,0 +1,390 @@ +# SimpleX Chat iOS -- Push Notification Service + +> Technical specification for the notification system: NtfManager, Notification Service Extension (NSE), notification modes, and token lifecycle. +> +> Related specs: [Architecture](../architecture.md) | [API Reference](../api.md) | [Navigation](../client/navigation.md) | [README](../README.md) +> Related product: [Product Overview](../../product/README.md) + +**Source:** [`NtfManager.swift`](../../Shared/Model/NtfManager.swift) | [`BGManager.swift`](../../Shared/Model/BGManager.swift) | [`Notifications.swift`](../../SimpleXChat/Notifications.swift) | [`NotificationService.swift`](../../SimpleX NSE/NotificationService.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Notification Modes](#2-notification-modes) +3. [NtfManager](#3-ntfmanager) +4. [Notification Service Extension (NSE)](#4-notification-service-extension) +5. [Token Lifecycle](#5-token-lifecycle) +6. [Notification Categories & Actions](#6-notification-categories--actions) +7. [Badge Management](#7-badge-management) +8. [Background Tasks (BGManager)](#8-background-tasks) + +--- + +## 1. Overview + +SimpleX Chat uses a privacy-preserving notification architecture. Because messages are end-to-end encrypted and the notification server never sees message content, the app uses a Notification Service Extension (NSE) to decrypt push payloads on-device before displaying notifications. + +``` +APNs Push → NSE receives encrypted payload + → NSE starts Haskell core (own chat_ctrl) + → NSE decrypts message using stored keys + → NSE creates UNNotificationContent with decrypted preview + → iOS displays notification to user +``` + +The notification system has three modes of operation, allowing users to choose their privacy/convenience tradeoff. + +--- + +## 2. Notification Modes + +| Mode | Description | Mechanism | +|------|-------------|-----------| +| **Instant** | Real-time notifications via Apple Push | APNs push triggers NSE, which decrypts and displays | +| **Periodic** | Background fetch every ~20 minutes | `BGAppRefreshTask` wakes app, checks for new messages | +| **Off** | No notifications | User must open app to see messages | + +### Configuration + +Notification mode is set via: +```swift +ChatCommand.apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) +``` + +`NotificationsMode` enum: `.instant`, `.periodic`, `.off` + +The mode is stored in `ChatModel.notificationMode` and persisted in `GroupDefaults`. + +--- + +## 3. NtfManager + +**File**: [`Shared/Model/NtfManager.swift`](../../Shared/Model/NtfManager.swift) + +Central notification coordinator. Singleton: `NtfManager.shared`. + +### [Class Definition](../../Shared/Model/NtfManager.swift#L27) + +```swift +class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { + static let shared = NtfManager() + public var navigatingToChat = false + private var granted = false + private var prevNtfTime: Dictionary = [:] +} +``` + +### Key Responsibilities + +| Method | Purpose | Line | +|--------|---------|------| +| [`registerCategories()`](../../Shared/Model/NtfManager.swift#L156) | Registers notification action categories with iOS | [156](../../Shared/Model/NtfManager.swift#L156) | +| [`requestAuthorization()`](../../Shared/Model/NtfManager.swift#L215) | Requests notification permission from user | [215](../../Shared/Model/NtfManager.swift#L215) | +| [`setNtfBadgeCount(_:)`](../../Shared/Model/NtfManager.swift#L264) | Updates app icon badge | [264](../../Shared/Model/NtfManager.swift#L264) | +| [`processNotificationResponse(_:)`](../../Shared/Model/NtfManager.swift#L54) | Handles user interaction with notification | [54](../../Shared/Model/NtfManager.swift#L54) | +| [`notifyContactRequest(_:)`](../../Shared/Model/NtfManager.swift#L239) | Shows contact request notification | [239](../../Shared/Model/NtfManager.swift#L239) | +| [`notifyCallInvitation(_:)`](../../Shared/Model/NtfManager.swift#L258) | Shows incoming call notification | [258](../../Shared/Model/NtfManager.swift#L258) | +| [`notifyMessageReceived(_:)`](../../Shared/Model/NtfManager.swift#L250) | Shows message received notification | [250](../../Shared/Model/NtfManager.swift#L250) | + +### [Notification Response Processing](../../Shared/Model/NtfManager.swift#L40) + +When user taps a notification: + +1. `userNotificationCenter(didReceive:)` delegate method fires +2. If app is active: calls `processNotificationResponse()` immediately +3. If app is inactive: stores in `ChatModel.notificationResponse` for later processing +4. [`processNotificationResponse()`](../../Shared/Model/NtfManager.swift#L54): + - Extracts `userId` from `userInfo` -- switches user if needed + - Extracts `chatId` -- navigates to the conversation + - Handles action identifiers (accept contact, accept/reject call) + +### [Rate Limiting](../../Shared/Model/NtfManager.swift#L144) + +`prevNtfTime` dictionary prevents notification flooding: +- Each chat has a timestamp of its last notification +- New notifications are suppressed if within `ntfTimeInterval` (1 second) of the previous one for the same chat + +--- + +## 4. Notification Service Extension (NSE) + +**File**: [`SimpleX NSE/NotificationService.swift`](../../SimpleX NSE/NotificationService.swift) + +### Architecture + +The NSE is a separate process that iOS launches when a push notification arrives. It has: +- Its own Haskell runtime instance (`chat_ctrl`) +- Shared database access (via app group container) +- ~30 second execution window per notification +- No access to main app's in-memory state + +### [Processing Flow](../../SimpleX NSE/NotificationService.swift#L300) + +``` +1. didReceive(request:, withContentHandler:) L300 + ├── 2. Initialize Haskell core (if not already running) + │ └── chat_migrate_init_key() with shared DB path L861 + ├── 3. Decode encrypted notification payload + │ └── apiGetNtfConns(nonce:, encNtfInfo:) L1123 + ├── 4. Fetch and decrypt messages + │ └── apiGetConnNtfMessages(connMsgReqs:) L1140 + ├── 5. Create notification content + │ ├── Contact name as title + │ ├── Decrypted message preview as body + │ └── Thread identifier for grouping + └── 6. Deliver to content handler +``` + +### NSE Commands + +The NSE uses a subset of the chat API: + +| Command | Purpose | Line | +|---------|---------|------| +| [`apiGetNtfConns(nonce:, encNtfInfo:)`](../../SimpleX NSE/NotificationService.swift#L1123) | Decrypt notification connection info | [1123](../../SimpleX NSE/NotificationService.swift#L1123) | +| [`apiGetConnNtfMessages(connMsgReqs:)`](../../SimpleX NSE/NotificationService.swift#L1140) | Fetch messages for notification connections | [1140](../../SimpleX NSE/NotificationService.swift#L1140) | + +### Database Coordination + +- NSE checks `appStateGroupDefault` before processing +- If main app is `.active`, NSE may skip processing (main app handles notifications directly) +- NSE uses `chat_close_store` / `chat_reopen_store` for safe concurrent access + +### [Preview Modes](../../SimpleXChat/APITypes.swift#L664) + +`NotificationPreviewMode` controls what the NSE shows: + +| Mode | Title | Body | +|------|-------|------| +| `.message` | Contact name | Message text | +| `.contact` | Contact name | "New message" | +| `.hidden` | "SimpleX" | "New message" | + +### Key Internal Types + +| Type | Purpose | Line | +|------|---------|------| +| [`NSENotificationData`](../../SimpleX NSE/NotificationService.swift#L27) | Enum of possible notification payloads | [27](../../SimpleX NSE/NotificationService.swift#L27) | +| [`NSEThreads`](../../SimpleX NSE/NotificationService.swift#L82) | Concurrency coordinator for multiple NSE instances | [82](../../SimpleX NSE/NotificationService.swift#L82) | +| [`NotificationEntity`](../../SimpleX NSE/NotificationService.swift#L245) | Per-connection processing state | [245](../../SimpleX NSE/NotificationService.swift#L245) | +| [`NotificationService`](../../SimpleX NSE/NotificationService.swift#L287) | Main NSE class (`UNNotificationServiceExtension`) | [287](../../SimpleX NSE/NotificationService.swift#L287) | +| [`NSEChatState`](../../SimpleX NSE/NotificationService.swift#L781) | Singleton managing NSE lifecycle state | [781](../../SimpleX NSE/NotificationService.swift#L781) | + +### Key Internal Functions + +| Function | Purpose | Line | +|----------|---------|------| +| [`startChat()`](../../SimpleX NSE/NotificationService.swift#L836) | Initializes Haskell core for NSE | [836](../../SimpleX NSE/NotificationService.swift#L836) | +| [`doStartChat()`](../../SimpleX NSE/NotificationService.swift#L861) | Performs actual chat initialization (migration, config) | [861](../../SimpleX NSE/NotificationService.swift#L861) | +| [`activateChat()`](../../SimpleX NSE/NotificationService.swift#L907) | Reactivates suspended chat controller | [907](../../SimpleX NSE/NotificationService.swift#L907) | +| [`suspendChat(_:)`](../../SimpleX NSE/NotificationService.swift#L921) | Suspends chat controller with timeout | [921](../../SimpleX NSE/NotificationService.swift#L921) | +| [`receiveMessages()`](../../SimpleX NSE/NotificationService.swift#L954) | Main message-receive loop | [954](../../SimpleX NSE/NotificationService.swift#L954) | +| [`receivedMsgNtf(_:)`](../../SimpleX NSE/NotificationService.swift#L1003) | Maps chat events to notification data | [1003](../../SimpleX NSE/NotificationService.swift#L1003) | +| [`receiveNtfMessages(_:)`](../../SimpleX NSE/NotificationService.swift#L403) | Orchestrates notification message fetch and delivery | [403](../../SimpleX NSE/NotificationService.swift#L403) | +| [`deliverBestAttemptNtf()`](../../SimpleX NSE/NotificationService.swift#L604) | Delivers the best available notification content | [604](../../SimpleX NSE/NotificationService.swift#L604) | +| [`didReceive(_:withContentHandler:)`](../../SimpleX%20NSE/NotificationService.swift#L300) | Main NSE entry point -- processes incoming notification | [300](../../SimpleX%20NSE/NotificationService.swift#L300) | + +--- + +## 5. Token Lifecycle + +### Registration Flow + +``` +1. App starts → AppDelegate.didRegisterForRemoteNotificationsWithDeviceToken + └── ChatModel.deviceToken = token + +2. Token registration (when chat running and token available): + └── apiRegisterToken(token, notificationMode) + └── Response: ntfToken(token, status, ntfMode, ntfServer) + └── ChatModel.tokenStatus = status + +3. Token verification (if server requires): + └── apiVerifyToken(token, nonce, code) + └── ChatModel.tokenRegistered = true + +4. Token check (periodic): + └── apiCheckToken(token) + └── Updates ChatModel.tokenStatus +``` + +### Token States (NtfTknStatus) + +| Status | Description | +|--------|-------------| +| `.new` | Token just registered, not yet verified | +| `.registered` | Token registered with notification server | +| `.confirmed` | Token confirmed and ready | +| `.active` | Token actively receiving notifications | +| `.expired` | Token expired, needs re-registration | +| `.invalid` | Token invalid, needs new registration | +| `.invalidBad` | Token invalid due to bad data | +| `.invalidTopic` | Token invalid due to wrong topic | +| `.invalidExpired` | Token invalid because it expired | +| `.invalidUnregistered` | Token invalid, was unregistered | + +### Token Deletion + +```swift +ChatCommand.apiDeleteToken(token: DeviceToken) +``` + +Called when: +- User switches to `.off` notification mode +- User deletes their profile +- Token becomes invalid and needs replacement + +--- + +## 6. Notification Categories & Actions + +Registered in [`NtfManager.registerCategories()`](../../Shared/Model/NtfManager.swift#L156): + +### Contact Request Category + +```swift +// Category: "NTF_CAT_CONTACT_REQUEST" +// Actions: +// - "NTF_ACT_ACCEPT_CONTACT": Accept contact request +``` + +When user taps "Accept" on a contact request notification: +1. `processNotificationResponse()` detects `ntfActionAcceptContact` +2. Calls `apiAcceptContact(incognito: false, contactReqId:)` +3. Navigates to the new contact's chat + +### Call Invitation Category + +```swift +// Category: "NTF_CAT_CALL_INVITATION" +// Actions: +// - "NTF_ACT_ACCEPT_CALL": Accept incoming call +// - "NTF_ACT_REJECT_CALL": Reject incoming call +``` + +When user taps "Accept" / "Reject" on a call notification: +1. `processNotificationResponse()` detects the action +2. Sets `ChatModel.ntfCallInvitationAction = (chatId, .accept/.reject)` +3. Call controller picks up the pending action + +### Message Category + +Standard tap-to-open behavior navigates to the chat. + +### Many Events Category + +Batch notification for multiple events -- navigates to the app without specific chat context. + +--- + +## 7. Badge Management + +The app icon badge shows the total unread message count: + +```swift +// Updated when: +// 1. App enters background: +NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers()) + +// 2. Messages are read: +// Badge is recalculated and updated + +// 3. NSE receives notification: +// NSE updates badge based on its count +``` + +`totalUnreadCountForAllUsers()` sums unread counts across all user profiles (not just the active user). + +### NSE Badge Handling + +| Method | Purpose | Line | +|--------|---------|------| +| [`setBadgeCount()`](../../SimpleX NSE/NotificationService.swift#L592) | Increments badge via `ntfBadgeCountGroupDefault` | [592](../../SimpleX NSE/NotificationService.swift#L592) | +| [`setNtfBadgeCount(_:)`](../../Shared/Model/NtfManager.swift#L264) | Sets badge on `UIApplication` | [264](../../Shared/Model/NtfManager.swift#L264) | +| [`changeNtfBadgeCount(by:)`](../../Shared/Model/NtfManager.swift#L270) | Adjusts badge by delta | [270](../../Shared/Model/NtfManager.swift#L270) | + +--- + +## 8. Background Tasks + +**File**: [`Shared/Model/BGManager.swift`](../../Shared/Model/BGManager.swift) + +### [BGManager](../../Shared/Model/BGManager.swift#L30) + +```swift +class BGManager { + static let shared = BGManager() + func register() // Register BGAppRefreshTask handlers + func schedule() // Schedule next background refresh +} +``` + +| Method | Purpose | Line | +|--------|---------|------| +| [`register()`](../../Shared/Model/BGManager.swift#L38) | Registers `BGAppRefreshTask` handler with iOS | [38](../../Shared/Model/BGManager.swift#L38) | +| [`schedule()`](../../Shared/Model/BGManager.swift#L46) | Schedules next background refresh request | [46](../../Shared/Model/BGManager.swift#L46) | +| [`handleRefresh(_:)`](../../Shared/Model/BGManager.swift#L74) | Processes background refresh task | [74](../../Shared/Model/BGManager.swift#L74) | +| [`completionHandler(_:)`](../../Shared/Model/BGManager.swift#L95) | Creates completion callback with cleanup | [95](../../Shared/Model/BGManager.swift#L95) | +| [`receiveMessages(_:)`](../../Shared/Model/BGManager.swift#L112) | Activates chat and receives pending messages | [112](../../Shared/Model/BGManager.swift#L112) | + +### Background Refresh (Periodic Mode) + +When notification mode is `.periodic`: + +1. `BGManager.schedule()` is called when app enters background +2. iOS wakes the app in the background approximately every 20 minutes +3. `BGAppRefreshTask` handler: + - Activates the chat engine: `apiActivateChat(restoreChat: true)` + - Checks for new messages + - Creates local notifications for any new messages + - Suspends chat: `apiSuspendChat(timeoutMicroseconds:)` + - Schedules next refresh +4. Must complete within ~30 seconds or iOS terminates the task + +### Background Task Protection + +All API calls use `beginBGTask()` / `endBackgroundTask()` to request extra execution time: + +```swift +func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) { + var id: UIBackgroundTaskIdentifier! + // ... + id = UIApplication.shared.beginBackgroundTask(expirationHandler: endTask) + return endTask +} +``` + +Maximum task duration: `maxTaskDuration = 15` seconds. + +--- + +## Notification Content Builders + +**File**: [`SimpleXChat/Notifications.swift`](../../SimpleXChat/Notifications.swift) + +| Function | Purpose | Line | +|----------|---------|------| +| [`createContactRequestNtf()`](../../SimpleXChat/Notifications.swift#L27) | Builds notification for incoming contact request | [L27](../../SimpleXChat/Notifications.swift#L27) | +| [`createContactConnectedNtf()`](../../SimpleXChat/Notifications.swift#L46) | Builds notification for contact connected event | [L46](../../SimpleXChat/Notifications.swift#L46) | +| [`createMessageReceivedNtf()`](../../SimpleXChat/Notifications.swift#L66) | Builds notification for received message | [L66](../../SimpleXChat/Notifications.swift#L66) | +| [`createCallInvitationNtf()`](../../SimpleXChat/Notifications.swift#L86) | Builds notification for incoming call | [L86](../../SimpleXChat/Notifications.swift#L86) | +| [`createConnectionEventNtf()`](../../SimpleXChat/Notifications.swift#L102) | Builds notification for connection events | [L102](../../SimpleXChat/Notifications.swift#L102) | +| [`createErrorNtf()`](../../SimpleXChat/Notifications.swift#L134) | Builds notification for database/encryption errors | [L134](../../SimpleXChat/Notifications.swift#L134) | +| [`createAppStoppedNtf()`](../../SimpleXChat/Notifications.swift#L160) | Builds notification when app is stopped | [L160](../../SimpleXChat/Notifications.swift#L160) | +| [`createNotification()`](../../SimpleXChat/Notifications.swift#L175) | Generic notification builder (used by all above) | [L175](../../SimpleXChat/Notifications.swift#L175) | +| [`hideSecrets()`](../../SimpleXChat/Notifications.swift#L200) | Redacts secret-formatted text in previews | [L200](../../SimpleXChat/Notifications.swift#L200) | + +--- + +## Source Files + +| File | Path | +|------|------| +| Notification manager | [`Shared/Model/NtfManager.swift`](../../Shared/Model/NtfManager.swift) | +| Background manager | [`Shared/Model/BGManager.swift`](../../Shared/Model/BGManager.swift) | +| Notification types | [`SimpleXChat/Notifications.swift`](../../SimpleXChat/Notifications.swift) | +| NSE service | [`SimpleX NSE/NotificationService.swift`](../../SimpleX NSE/NotificationService.swift) | +| App delegate (token) | `Shared/AppDelegate.swift` | +| Notification settings UI | `Shared/Views/UserSettings/NotificationsView.swift` | diff --git a/apps/ios/spec/services/theme.md b/apps/ios/spec/services/theme.md new file mode 100644 index 0000000000..321f3307f9 --- /dev/null +++ b/apps/ios/spec/services/theme.md @@ -0,0 +1,383 @@ +# SimpleX Chat iOS -- Theme Engine + +> Technical specification for the theming system: ThemeManager, default themes, customization layers, wallpapers, and YAML export. +> +> Related specs: [State Management](../state.md) | [Architecture](../architecture.md) | [README](../README.md) +> Related product: [Product Overview](../../product/README.md) + +**Source:** [`ThemeManager.swift`](../../Shared/Theme/ThemeManager.swift) | [`AppearanceSettings.swift`](../../Shared/Views/UserSettings/AppearanceSettings.swift) | [`ThemeTypes.swift`](../../SimpleXChat/Theme/ThemeTypes.swift) | [`ChatWallpaperTypes.swift`](../../SimpleXChat/Theme/ChatWallpaperTypes.swift) | [`Theme.swift`](../../Shared/Theme/Theme.swift) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ThemeManager](#2-thememanager) +3. [Default Themes](#3-default-themes) +4. [Customization Layers](#4-customization-layers) +5. [Color System](#5-color-system) +6. [Wallpapers](#6-wallpapers) +7. [Chat Bubble Styling](#7-chat-bubble-styling) +8. [Color Scheme Mode](#8-color-scheme-mode) +9. [YAML Export/Import](#9-yaml-exportimport) + +--- + +## 1. Overview + +The theme engine provides a layered customization system where themes can be overridden at multiple levels: global defaults, per-user, and per-chat. + +``` +Theme Resolution Order (most specific wins): +┌─────────────────────┐ +│ Per-chat override │ apiSetChatUIThemes(chatId:, themes:) +├─────────────────────┤ +│ Per-user override │ apiSetUserUIThemes(userId:, themes:) +├─────────────────────┤ +│ App settings theme │ themeOverridesDefault (UserDefaults) +├─────────────────────┤ +│ Base theme │ Light / Dark / SimpleX / Black +└─────────────────────┘ +``` + +The resolved theme is published as `AppTheme.shared` and consumed by all SwiftUI views via `@EnvironmentObject`. + +--- + +## 2. [ThemeManager](../../Shared/Theme/ThemeManager.swift) (L15) + +**File**: [`Shared/Theme/ThemeManager.swift`](../../Shared/Theme/ThemeManager.swift) + +Static utility class that resolves the current theme by merging all customization layers. + +### [ActiveTheme](../../Shared/Theme/ThemeManager.swift#L17) + +The resolved theme output: + +```swift +struct ActiveTheme: Equatable { + let name: String // Theme name (e.g., "light", "dark", "simplex", "black", "system") + let base: DefaultTheme // Base theme enum + let colors: Colors // Resolved color palette + let appColors: AppColors // App-specific colors (sent/received bubbles, etc.) + var wallpaper: AppWallpaper // Resolved wallpaper +} +``` + +### Key Static Methods + +| Method | Purpose | Line | +|--------|---------|------| +| [`applyTheme(_:)`](../../Shared/Theme/ThemeManager.swift#L124) | Apply a theme by name, updates `AppTheme.shared` | [L124](../../Shared/Theme/ThemeManager.swift#L124) | +| [`currentColors(...)`](../../Shared/Theme/ThemeManager.swift#L64) | Resolve full theme from all layers | [L64](../../Shared/Theme/ThemeManager.swift#L64) | +| [`defaultActiveTheme(_:)`](../../Shared/Theme/ThemeManager.swift#L48) | Get default theme override from app settings | [L48](../../Shared/Theme/ThemeManager.swift#L48) | +| [`currentThemeOverridesForExport(...)`](../../Shared/Theme/ThemeManager.swift#L105) | Get current overrides for YAML export | [L105](../../Shared/Theme/ThemeManager.swift#L105) | +| [`adjustWindowStyle()`](../../Shared/Theme/ThemeManager.swift#L136) | Adjust window style after theme change | [L136](../../Shared/Theme/ThemeManager.swift#L136) | +| [`changeDarkTheme(_:)`](../../Shared/Theme/ThemeManager.swift#L166) | Change the dark theme variant | [L166](../../Shared/Theme/ThemeManager.swift#L166) | +| [`saveAndApplyThemeColor(...)`](../../Shared/Theme/ThemeManager.swift#L173) | Save and apply a theme color override | [L173](../../Shared/Theme/ThemeManager.swift#L173) | +| [`applyThemeColor(...)`](../../Shared/Theme/ThemeManager.swift#L186) | Apply a theme color to a binding | [L186](../../Shared/Theme/ThemeManager.swift#L186) | +| [`saveAndApplyWallpaper(...)`](../../Shared/Theme/ThemeManager.swift#L191) | Save and apply a wallpaper change | [L191](../../Shared/Theme/ThemeManager.swift#L191) | +| [`copyFromSameThemeOverrides(...)`](../../Shared/Theme/ThemeManager.swift#L213) | Copy overrides from matching theme | [L213](../../Shared/Theme/ThemeManager.swift#L213) | +| [`applyWallpaper(...)`](../../Shared/Theme/ThemeManager.swift#L256) | Apply wallpaper to a binding | [L256](../../Shared/Theme/ThemeManager.swift#L256) | +| [`saveAndApplyThemeOverrides(...)`](../../Shared/Theme/ThemeManager.swift#L267) | Save and apply full theme overrides | [L267](../../Shared/Theme/ThemeManager.swift#L267) | +| [`resetAllThemeColors(_:)`](../../Shared/Theme/ThemeManager.swift#L288) | Reset all color overrides (CodableDefault) | [L288](../../Shared/Theme/ThemeManager.swift#L288) | +| [`resetAllThemeColors(_:)`](../../Shared/Theme/ThemeManager.swift#L302) | Reset all color overrides (Binding) | [L302](../../Shared/Theme/ThemeManager.swift#L302) | +| [`removeTheme(_:)`](../../Shared/Theme/ThemeManager.swift#L311) | Remove a saved theme by ID | [L311](../../Shared/Theme/ThemeManager.swift#L311) | + +### Theme Resolution Algorithm + +[`currentColors()`](../../Shared/Theme/ThemeManager.swift#L64) in `ThemeManager.swift`: + +1. Determine base theme from `currentThemeDefault`: + - If `"system"`: use light or dark based on [`systemInDarkThemeCurrently`](../../Shared/Theme/Theme.swift#L95) + - Dark mode maps to `systemDarkThemeDefault` (Dark, SimpleX, or Black) +2. Get base color palette ([`LightColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L650), [`DarkColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L629), [`SimplexColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L671), [`BlackColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L692)) +3. Look up app settings theme override (`themeOverridesDefault`) +4. Look up per-user theme override (`User.uiThemes`) +5. Look up per-chat theme override (from ChatInfo) +6. Look up wallpaper preset colors (if wallpaper has preset color overrides) +7. Merge layers: base <- app override <- preset wallpaper colors <- per-user <- per-chat +8. Return `ActiveTheme` with resolved colors, app colors, and wallpaper + +--- + +## 3. Default Themes + +Four built-in themes with pre-defined color palettes: + +| Theme | Enum | Key Characteristics | +|-------|------|---------------------| +| **Light** | `DefaultTheme.LIGHT` | White background, standard colors | +| **Dark** | `DefaultTheme.DARK` | Dark gray background, light text | +| **SimpleX** | `DefaultTheme.SIMPLEX` | Brand purple accents, dark background | +| **Black** | `DefaultTheme.BLACK` | Pure black background (OLED), high contrast | + +### [DefaultTheme](../../SimpleXChat/Theme/ThemeTypes.swift#L13) Enum + +```swift +enum DefaultTheme { + case LIGHT + case DARK + case SIMPLEX + case BLACK + + static let SYSTEM_THEME_NAME = "SYSTEM" + + var themeName: String { ... } + var mode: DefaultThemeMode { ... } // .light or .dark +} +``` + +### Color Palettes + +Each base theme defines two palette types: +- [`Colors`](../../SimpleXChat/Theme/ThemeTypes.swift#L44): Standard UI colors (primary, background, surface, error, onBackground, onSurface) +- [`AppColors`](../../SimpleXChat/Theme/ThemeTypes.swift#L90): App-specific colors (sentMessage, receivedMessage, title, primaryVariant2) + +Palette instances: +- [`LightColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L650) / [`LightColorPaletteApp`](../../SimpleXChat/Theme/ThemeTypes.swift#L662) +- [`DarkColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L629) / [`DarkColorPaletteApp`](../../SimpleXChat/Theme/ThemeTypes.swift#L641) +- [`SimplexColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L671) / [`SimplexColorPaletteApp`](../../SimpleXChat/Theme/ThemeTypes.swift#L683) +- [`BlackColorPalette`](../../SimpleXChat/Theme/ThemeTypes.swift#L692) / [`BlackColorPaletteApp`](../../SimpleXChat/Theme/ThemeTypes.swift#L704) + +--- + +## 4. Customization Layers + +### Layer 1: App Settings Theme + +Stored in `themeOverridesDefault` (UserDefaults). Contains `[ThemeOverrides]` -- an array of theme overrides, one per base theme. + +#### [`ThemeOverrides`](../../SimpleXChat/Theme/ThemeTypes.swift#L385) + +```swift +struct ThemeOverrides: Codable { + var base: DefaultTheme + var colors: ThemeColors? // Color overrides + var wallpaper: ThemeWallpaper? // Wallpaper setting +} +``` + +### Layer 2: Per-User Theme + +Stored on the `User` object (`User.uiThemes: ThemeModeOverrides?`), persisted in the Haskell database via `apiSetUserUIThemes(userId:, themes:)`. + +#### [`ThemeModeOverrides`](../../SimpleXChat/Theme/ThemeTypes.swift#L570) + +```swift +struct ThemeModeOverrides: Codable { + var light: ThemeModeOverride? + var dark: ThemeModeOverride? +} +``` + +#### [`ThemeModeOverride`](../../SimpleXChat/Theme/ThemeTypes.swift#L585) + +```swift +struct ThemeModeOverride: Codable { + var mode: DefaultThemeMode? + var colors: ThemeColors? + var wallpaper: ThemeWallpaper? + var type: WallpaperType? // Computed from wallpaper +} +``` + +### Layer 3: Per-Chat Theme + +Stored per-chat via `apiSetChatUIThemes(chatId:, themes:)`. Same `ThemeModeOverrides` structure. + +### Override Merging + +Colors are merged field-by-field: if a more-specific layer defines a color, it overrides; if nil, falls through to the next layer. + +--- + +## 5. Color System + +**File**: [`SimpleXChat/Theme/ThemeTypes.swift`](../../SimpleXChat/Theme/ThemeTypes.swift) + +### [ThemeColors](../../SimpleXChat/Theme/ThemeTypes.swift#L230) + +Overridable color definitions: + +```swift +struct ThemeColors: Codable { + var primary: String? // Primary brand color + var primaryVariant: String? // Primary variant + var secondary: String? // Secondary color + var secondaryVariant: String? // Secondary variant + var background: String? // Main background + var surface: String? // Card/surface background + var title: String? // Title text color + var primaryVariant2: String? // Additional variant + var sentMessage: String? // Sent message bubble + var sentQuote: String? // Sent quote background + var receivedMessage: String? // Received message bubble + var receivedQuote: String? // Received quote background +} +``` + +Colors are stored as hex strings (e.g., `"#FF6600"`) and converted to SwiftUI `Color` values at resolution time. + +### [Colors](../../SimpleXChat/Theme/ThemeTypes.swift#L44) (Resolved Palette) + +```swift +struct Colors { + var isLight: Bool + var primary: Color + var primaryVariant: Color + var secondary: Color + var secondaryVariant: Color + var background: Color + var surface: Color + var error: Color + var onBackground: Color + var onSurface: Color + // ... etc +} +``` + +### [AppColors](../../SimpleXChat/Theme/ThemeTypes.swift#L90) (Resolved App-Specific) + +```swift +struct AppColors { + var title: Color + var primaryVariant2: Color + var sentMessage: Color + var sentQuote: Color + var receivedMessage: Color + var receivedQuote: Color +} +``` + +--- + +## 6. Wallpapers + +**File**: [`SimpleXChat/Theme/ChatWallpaperTypes.swift`](../../SimpleXChat/Theme/ChatWallpaperTypes.swift) + +### [Preset Wallpapers](../../SimpleXChat/Theme/ChatWallpaperTypes.swift#L13) + +6 built-in wallpaper presets: + +| Preset | ID | Description | +|--------|-----|-------------| +| Cats | `cats` | Cat-themed pattern | +| Flowers | `flowers` | Floral pattern | +| Hearts | `hearts` | Heart pattern | +| Kids | `kids` | Children's pattern | +| School | `school` | School/notebook pattern (default) | +| Travel | `travel` | Travel-themed pattern | + +Each preset defines per-theme color tints (`PresetWallpaper.colors[DefaultTheme]`) that subtly adjust the color palette to complement the wallpaper. + +### Custom Wallpapers + +Users can set a custom image as wallpaper: +- Stored in `Documents/wallpapers/` directory +- Scaled and tiled to fill the chat background +- Custom wallpapers can be combined with color overrides + +### [WallpaperType](../../SimpleXChat/Theme/ChatWallpaperTypes.swift#L311) + +```swift +enum WallpaperType { + case preset(filename: String, scale: Float?) // Built-in wallpaper + case image(filename: String, scale: Float?) // Custom image + case empty // No wallpaper +} +``` + +### [AppWallpaper](../../SimpleXChat/Theme/ThemeTypes.swift#L142) (Resolved) + +```swift +struct AppWallpaper { + var background: Color? // Background color override + var tint: Color? // Tint/overlay color + var type: WallpaperType +} +``` + +--- + +## 7. Chat Bubble Styling + +Configurable bubble appearance properties: + +| Property | Description | Stored In | +|----------|-------------|-----------| +| `chatItemRoundness` | Corner radius of message bubbles | App settings | +| `chatItemTail` | Whether bubbles have a tail/arrow | App settings | +| Avatar corner radius | Roundness of profile avatars | App settings | + +These are configured in [`Shared/Views/UserSettings/AppearanceSettings.swift`](../../Shared/Views/UserSettings/AppearanceSettings.swift) ([L26](../../Shared/Views/UserSettings/AppearanceSettings.swift#L26)). + +--- + +## 8. Color Scheme Mode + +### System Follow + +When theme is set to `"system"` (DefaultTheme.SYSTEM_THEME_NAME): +- Light mode: uses `DefaultTheme.LIGHT` palette +- Dark mode: uses the configured dark theme (`systemDarkThemeDefault`), which can be Dark, SimpleX, or Black + +### Forced Mode + +Users can force light or dark mode regardless of system setting by selecting a specific theme other than "system". + +### Detection + +[`systemInDarkThemeCurrently`](../../Shared/Theme/Theme.swift#L95): + +```swift +var systemInDarkThemeCurrently: Bool { + return UITraitCollection.current.userInterfaceStyle == .dark +} +``` + +`ChatModel.currentUser` setter triggers [`ThemeManager.applyTheme()`](../../Shared/Theme/ThemeManager.swift#L124) to handle per-user theme overrides when switching users. + +--- + +## 9. YAML Export/Import + +Theme configurations can be exported as YAML for sharing: + +### Export + +[`ThemeManager.currentThemeOverridesForExport()`](../../Shared/Theme/ThemeManager.swift#L105) generates a `ThemeOverrides` representing the current resolved theme, which is then serialized to YAML using the Yams library. + +### Import + +YAML theme strings are parsed back into `ThemeOverrides` and applied as app settings theme overrides. + +Key functions in [`AppearanceSettings.swift`](../../Shared/Views/UserSettings/AppearanceSettings.swift): + +| Function | Purpose | Line | +|----------|---------|------| +| [`ImportExportThemeSection`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L603) | UI section for import/export controls | [L603](../../Shared/Views/UserSettings/AppearanceSettings.swift#L603) | +| [`ThemeImporter`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L640) | ViewModifier for YAML file import | [L640](../../Shared/Views/UserSettings/AppearanceSettings.swift#L640) | +| [`decodeYAML(_:)`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L1150) | Parse YAML string into Decodable type | [L1150](../../Shared/Views/UserSettings/AppearanceSettings.swift#L1150) | +| [`encodeThemeOverrides(_:)`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L1160) | Encode ThemeOverrides to YAML string | [L1160](../../Shared/Views/UserSettings/AppearanceSettings.swift#L1160) | + +### Toolbar Material + +[`ToolbarMaterial`](../../Shared/Views/UserSettings/AppearanceSettings.swift#L319) controls the navigation bar appearance: +- Configurable opacity/material (translucent, opaque) +- Stored in app settings + +--- + +## Source Files + +| File | Path | Key Definitions | +|------|------|-----------------| +| Theme manager | [`Shared/Theme/ThemeManager.swift`](../../Shared/Theme/ThemeManager.swift) | `ThemeManager` (L15), `ActiveTheme` (L17) | +| Theme types & colors | [`SimpleXChat/Theme/ThemeTypes.swift`](../../SimpleXChat/Theme/ThemeTypes.swift) | `DefaultTheme` (L13), `Colors` (L44), `AppColors` (L90), `AppWallpaper` (L142), `ThemeColors` (L230), `ThemeWallpaper` (L302), `ThemeOverrides` (L385), `ThemeModeOverrides` (L570), `ThemeModeOverride` (L585) | +| Wallpaper types | [`SimpleXChat/Theme/ChatWallpaperTypes.swift`](../../SimpleXChat/Theme/ChatWallpaperTypes.swift) | `PresetWallpaper` (L13), `WallpaperType` (L311) | +| Color utilities | [`SimpleXChat/Theme/Color.swift`](../../SimpleXChat/Theme/Color.swift) | Hex color conversion | +| App theme observable | [`Shared/Theme/Theme.swift`](../../Shared/Theme/Theme.swift) | `AppTheme` (L22), `CurrentColors` (L14), `systemInDarkThemeCurrently` (L95) | +| Appearance settings UI | [`Shared/Views/UserSettings/AppearanceSettings.swift`](../../Shared/Views/UserSettings/AppearanceSettings.swift) | `AppearanceSettings` (L26), `ToolbarMaterial` (L319), `ImportExportThemeSection` (L603) | +| Theme mode editor | `Shared/Views/Helpers/ThemeModeEditor.swift` | Theme mode selection UI | +| Haskell theme types | `../../src/Simplex/Chat/Types/UITheme.hs` | Server-side theme persistence | diff --git a/apps/ios/spec/state.md b/apps/ios/spec/state.md new file mode 100644 index 0000000000..68b5f3cbcc --- /dev/null +++ b/apps/ios/spec/state.md @@ -0,0 +1,463 @@ +# SimpleX Chat iOS -- State Management + +**Source:** [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1375) | [`ChatTypes.swift`](../SimpleXChat/ChatTypes.swift#L1-L5284) + +> Technical specification for the app's state architecture: ChatModel, ItemsModel, Chat, ChatInfo, and preference storage. +> +> Related specs: [Architecture](architecture.md) | [API Reference](api.md) | [README](README.md) +> Related product: [Concept Index](../product/concepts.md) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatModel -- Primary App State](#2-chatmodel) +3. [ItemsModel -- Per-Chat Message State](#3-itemsmodel) +4. [ChatTagsModel -- Tag Filtering State](#4-chattagsmodel) +5. [Chat -- Single Conversation State](#5-chat) +6. [ChatInfo -- Conversation Metadata](#6-chatinfo) +7. [State Flow](#7-state-flow) +8. [Preference Storage](#8-preference-storage) + +--- + +## 1. Overview + +The app uses SwiftUI's `ObservableObject` pattern for reactive state management. The state hierarchy is: + +``` +ChatModel (singleton -- global app state) +├── currentUser: User? +├── users: [UserInfo] +├── chats: [Chat] (chat list) +├── chatId: String? (active chat ID) +├── im: ItemsModel.shared (primary chat items) +├── secondaryIM: ItemsModel? (secondary chat items, e.g. support scope) +├── activeCall: Call? +├── callInvitations: [ChatId: RcvCallInvitation] +├── deviceToken / savedToken / tokenStatus +├── notificationMode: NotificationsMode +├── onboardingStage: OnboardingStage? +├── migrationState: MigrationToState? +└── ... (50+ @Published properties) + +ItemsModel (singleton + secondary instances -- per-chat message state) +├── reversedChatItems: [ChatItem] (messages in reverse order) +├── chatState: ActiveChatState (pagination/split state) +├── isLoading / showLoadingProgress +└── preloadState: PreloadState + +Chat (per-conversation -- one per entry in chat list) +├── chatInfo: ChatInfo (type + metadata) +├── chatItems: [ChatItem] (preview items) +└── chatStats: ChatStats (unread counts) + +ChatTagsModel (singleton -- filter state) +├── userTags: [ChatTag] +├── activeFilter: ActiveFilter? +├── presetTags: [PresetTag: Int] +└── unreadTags: [Int64: Int] +``` + +--- + +## 2. [ChatModel](../Shared/Model/ChatModel.swift#L337-L1260) + +**Class**: `final class ChatModel: ObservableObject` +**Singleton**: `ChatModel.shared` +**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L337) + +### Key Published Properties + +#### App Lifecycle +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `onboardingStage` | `OnboardingStage?` | Current onboarding step | [L331](../Shared/Model/ChatModel.swift#L338) | +| `chatInitialized` | `Bool` | Whether chat has been initialized | [L340](../Shared/Model/ChatModel.swift#L347) | +| `chatRunning` | `Bool?` | Whether chat engine is running | [L341](../Shared/Model/ChatModel.swift#L348) | +| `chatDbChanged` | `Bool` | Whether DB was changed externally | [L342](../Shared/Model/ChatModel.swift#L349) | +| `chatDbEncrypted` | `Bool?` | Whether DB is encrypted | [L343](../Shared/Model/ChatModel.swift#L350) | +| `chatDbStatus` | `DBMigrationResult?` | DB migration status | [L344](../Shared/Model/ChatModel.swift#L351) | +| `ctrlInitInProgress` | `Bool` | Whether controller is initializing | [L345](../Shared/Model/ChatModel.swift#L352) | +| `migrationState` | `MigrationToState?` | Device migration state | [L390](../Shared/Model/ChatModel.swift#L398) | + +#### User State +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `currentUser` | `User?` | Active user profile (triggers theme reapply on change) | [L334](../Shared/Model/ChatModel.swift#L341) | +| `users` | `[UserInfo]` | All user profiles | [L339](../Shared/Model/ChatModel.swift#L346) | +| `v3DBMigration` | `V3DBMigrationState` | Legacy DB migration state | [L333](../Shared/Model/ChatModel.swift#L340) | + +#### Chat List +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `chats` | `[Chat]` (private set) | All conversations for current user | [L351](../Shared/Model/ChatModel.swift#L358) | +| `deletedChats` | `Set` | Chat IDs pending deletion animation | [L352](../Shared/Model/ChatModel.swift#L359) | + +#### Active Chat +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `chatId` | `String?` | Currently open chat ID | [L354](../Shared/Model/ChatModel.swift#L361) | +| `chatAgentConnId` | `String?` | Agent connection ID for active chat | [L355](../Shared/Model/ChatModel.swift#L362) | +| `chatSubStatus` | `SubscriptionStatus?` | Active chat subscription status | [L356](../Shared/Model/ChatModel.swift#L363) | +| `openAroundItemId` | `ChatItem.ID?` | Item to scroll to when opening | [L357](../Shared/Model/ChatModel.swift#L364) | +| `chatToTop` | `String?` | Chat to scroll to top | [L358](../Shared/Model/ChatModel.swift#L365) | +| `groupMembers` | `[GMember]` | Members of active group | [L359](../Shared/Model/ChatModel.swift#L366) | +| `groupMembersIndexes` | `[Int64: Int]` | Member ID to index mapping | [L360](../Shared/Model/ChatModel.swift#L367) | +| `membersLoaded` | `Bool` | Whether members have been loaded | [L361](../Shared/Model/ChatModel.swift#L368) | +| `secondaryIM` | `ItemsModel?` | Secondary items model (e.g. support chat scope) | [L408](../Shared/Model/ChatModel.swift#L416) | + +#### Authentication +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `contentViewAccessAuthenticated` | `Bool` | Whether user has passed authentication | [L348](../Shared/Model/ChatModel.swift#L355) | +| `laRequest` | `LocalAuthRequest?` | Pending authentication request | [L349](../Shared/Model/ChatModel.swift#L356) | + +#### Notifications +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `deviceToken` | `DeviceToken?` | Current APNs device token | [L369](../Shared/Model/ChatModel.swift#L376) | +| `savedToken` | `DeviceToken?` | Previously saved token | [L370](../Shared/Model/ChatModel.swift#L377) | +| `tokenRegistered` | `Bool` | Whether token is registered with server | [L371](../Shared/Model/ChatModel.swift#L378) | +| `tokenStatus` | `NtfTknStatus?` | Token registration status | [L373](../Shared/Model/ChatModel.swift#L380) | +| `notificationMode` | `NotificationsMode` | Current notification mode (.off/.periodic/.instant) | [L374](../Shared/Model/ChatModel.swift#L381) | +| `notificationServer` | `String?` | Notification server URL | [L375](../Shared/Model/ChatModel.swift#L382) | +| `notificationPreview` | `NotificationPreviewMode` | What to show in notifications | [L376](../Shared/Model/ChatModel.swift#L383) | +| `notificationResponse` | `UNNotificationResponse?` | Pending notification action | [L346](../Shared/Model/ChatModel.swift#L353) | +| `ntfContactRequest` | `NTFContactRequest?` | Pending contact request from notification | [L378](../Shared/Model/ChatModel.swift#L385) | +| `ntfCallInvitationAction` | `(ChatId, NtfCallAction)?` | Pending call action from notification | [L379](../Shared/Model/ChatModel.swift#L386) | + +#### Calls +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `callInvitations` | `[ChatId: RcvCallInvitation]` | Pending incoming call invitations | [L381](../Shared/Model/ChatModel.swift#L388) | +| `activeCall` | `Call?` | Currently active call | [L382](../Shared/Model/ChatModel.swift#L389) | +| `callCommand` | `WebRTCCommandProcessor` | WebRTC command queue | [L383](../Shared/Model/ChatModel.swift#L390) | +| `showCallView` | `Bool` | Whether to show full-screen call UI | [L384](../Shared/Model/ChatModel.swift#L391) | +| `activeCallViewIsCollapsed` | `Bool` | Whether call view is in PiP mode | [L385](../Shared/Model/ChatModel.swift#L392) | + +#### Remote Desktop +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `remoteCtrlSession` | `RemoteCtrlSession?` | Active remote desktop session | [L387](../Shared/Model/ChatModel.swift#L395) | + +#### Misc +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `userAddress` | `UserContactLink?` | User's SimpleX address | [L365](../Shared/Model/ChatModel.swift#L372) | +| `chatItemTTL` | `ChatItemTTL` | Global message TTL | [L366](../Shared/Model/ChatModel.swift#L373) | +| `appOpenUrl` | `URL?` | URL opened while app active | [L367](../Shared/Model/ChatModel.swift#L374) | +| `appOpenUrlLater` | `URL?` | URL opened while app inactive | [L368](../Shared/Model/ChatModel.swift#L375) | +| `showingInvitation` | `ShowingInvitation?` | Currently displayed invitation | [L389](../Shared/Model/ChatModel.swift#L397) | +| `draft` | `ComposeState?` | Saved compose draft | [L393](../Shared/Model/ChatModel.swift#L401) | +| `draftChatId` | `String?` | Chat ID for saved draft | [L394](../Shared/Model/ChatModel.swift#L402) | +| `networkInfo` | `UserNetworkInfo` | Current network type and status | [L395](../Shared/Model/ChatModel.swift#L403) | +| `conditions` | `ServerOperatorConditions` | Server usage conditions | [L397](../Shared/Model/ChatModel.swift#L405) | +| `stopPreviousRecPlay` | `URL?` | Currently playing audio source | [L392](../Shared/Model/ChatModel.swift#L400) | + +### Non-Published Properties + +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `messageDelivery` | `[Int64: () -> Void]` | Pending delivery confirmation callbacks | [L399](../Shared/Model/ChatModel.swift#L407) | +| `filesToDelete` | `Set` | Files queued for deletion | [L401](../Shared/Model/ChatModel.swift#L409) | +| `im` | `ItemsModel` | Reference to `ItemsModel.shared` | [L405](../Shared/Model/ChatModel.swift#L413) | + +### Key Methods + +| Method | Description | Line | +|--------|-------------|------| +| `getUser(_ userId:)` | Find user by ID | [L427](../Shared/Model/ChatModel.swift#L436) | +| `updateUser(_ user:)` | Update user in list and current | [L437](../Shared/Model/ChatModel.swift#L447) | +| `removeUser(_ user:)` | Remove user from list | [L446](../Shared/Model/ChatModel.swift#L457) | +| `getChat(_ id:)` | Find chat by ID | [L456](../Shared/Model/ChatModel.swift#L468) | +| `addChat(_ chat:)` | Add chat to list | [L510](../Shared/Model/ChatModel.swift#L523) | +| `updateChatInfo(_ cInfo:)` | Update chat metadata | [L523](../Shared/Model/ChatModel.swift#L537) | +| `replaceChat(_ id:, _ chat:)` | Replace chat in list | [L574](../Shared/Model/ChatModel.swift#L589) | +| `removeChat(_ id:)` | Remove chat from list | [L1180](../Shared/Model/ChatModel.swift#L1198) | +| `popChat(_ id:, _ ts:)` | Move chat to top of list | [L1157](../Shared/Model/ChatModel.swift#L1174) | +| `totalUnreadCountForAllUsers()` | Sum unread across all users | [L1058](../Shared/Model/ChatModel.swift#L1074) | + +--- + +## 3. [ItemsModel](../Shared/Model/ChatModel.swift#L74-L174) + +**Class**: `class ItemsModel: ObservableObject` +**Primary singleton**: `ItemsModel.shared` +**Secondary instances**: Created via `ItemsModel.loadSecondaryChat()` for scope-based views (e.g., group member support chat) +**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L74) + +### Properties + +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `reversedChatItems` | `[ChatItem]` | Messages in reverse chronological order (newest first) | [L78](../Shared/Model/ChatModel.swift#L80) | +| `itemAdded` | `Bool` | Flag indicating a new item was added | [L81](../Shared/Model/ChatModel.swift#L83) | +| `chatState` | `ActiveChatState` | Pagination splits and loaded ranges | [L85](../Shared/Model/ChatModel.swift#L87) | +| `isLoading` | `Bool` | Whether messages are currently loading | [L89](../Shared/Model/ChatModel.swift#L91) | +| `showLoadingProgress` | `ChatId?` | Chat ID showing loading spinner | [L90](../Shared/Model/ChatModel.swift#L92) | +| `preloadState` | `PreloadState` | State for infinite-scroll preloading | [L75](../Shared/Model/ChatModel.swift#L77) | +| `secondaryIMFilter` | `SecondaryItemsModelFilter?` | Filter for secondary instances | [L74](../Shared/Model/ChatModel.swift#L76) | + +### Computed Properties + +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `lastItemsLoaded` | `Bool` | Whether the oldest messages have been loaded | [L95](../Shared/Model/ChatModel.swift#L97) | +| `contentTag` | `MsgContentTag?` | Content type filter (if secondary) | [L154](../Shared/Model/ChatModel.swift#L159) | +| `groupScopeInfo` | `GroupChatScopeInfo?` | Group scope filter (if secondary) | [L162](../Shared/Model/ChatModel.swift#L167) | + +### Throttling + +`ItemsModel` uses a custom publisher throttle (0.2 seconds) to batch rapid updates to `reversedChatItems` and prevent excessive SwiftUI re-renders: + +```swift +publisher + .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) + .sink { self.objectWillChange.send() } + .store(in: &bag) +``` + +Direct `@Published` properties (`isLoading`, `showLoadingProgress`) bypass throttling for immediate UI response. + +### Key Methods + +| Method | Description | Line | +|--------|-------------|------| +| `loadOpenChat(_ chatId:)` | Load chat with 250ms navigation delay | [L113](../Shared/Model/ChatModel.swift#L117) | +| `loadOpenChatNoWait(_ chatId:, _ openAroundItemId:)` | Load chat without delay | [L138](../Shared/Model/ChatModel.swift#L143) | +| `loadSecondaryChat(_ chatId:, chatFilter:)` | Create secondary ItemsModel instance | [L107](../Shared/Model/ChatModel.swift#L110) | + +### [SecondaryItemsModelFilter](../Shared/Model/ChatModel.swift#L58-L70) + +Used for secondary chat views (e.g., group member support scope, content type filter): + +```swift +enum SecondaryItemsModelFilter { + case groupChatScopeContext(groupScopeInfo: GroupChatScopeInfo) + case msgContentTagContext(contentTag: MsgContentTag) +} +``` + +--- + +## 4. [ChatTagsModel](../Shared/Model/ChatModel.swift#L189-L291) + +**Class**: `class ChatTagsModel: ObservableObject` +**Singleton**: `ChatTagsModel.shared` +**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L189) + +### Properties + +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `userTags` | `[ChatTag]` | User-defined tags | [L186](../Shared/Model/ChatModel.swift#L192) | +| `activeFilter` | `ActiveFilter?` | Currently active filter tab | [L187](../Shared/Model/ChatModel.swift#L193) | +| `presetTags` | `[PresetTag: Int]` | Preset tag counts (groups, contacts, favorites, etc.) | [L188](../Shared/Model/ChatModel.swift#L194) | +| `unreadTags` | `[Int64: Int]` | Unread count per user tag | [L189](../Shared/Model/ChatModel.swift#L195) | + +### [ActiveFilter](../Shared/Views/ChatList/ChatListView.swift#L52) + +```swift +enum ActiveFilter { + case presetTag(PresetTag) // .favorites, .contacts, .groups, .business, .groupReports + case userTag(ChatTag) // User-defined tag + case unread // Unread conversations +} +``` + +--- + +## 5. [Chat](../Shared/Model/ChatModel.swift#L1311-L1323) + +**Class**: `final class Chat: ObservableObject, Identifiable, ChatLike` +**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L1271) + +Represents a single conversation in the chat list. Each `Chat` is an independent observable object. + +### Properties + +| Property | Type | Description | Line | +|----------|------|-------------|------| +| `chatInfo` | `ChatInfo` | Conversation type and metadata | [L1253](../Shared/Model/ChatModel.swift#L1272) | +| `chatItems` | `[ChatItem]` | Preview items (typically last message) | [L1254](../Shared/Model/ChatModel.swift#L1273) | +| `chatStats` | `ChatStats` | Unread counts and min unread item ID | [L1255](../Shared/Model/ChatModel.swift#L1274) | +| `created` | `Date` | Creation timestamp | [L1256](../Shared/Model/ChatModel.swift#L1275) | + +### [ChatStats](../SimpleXChat/ChatTypes.swift#L1877-L1899) + +```swift +struct ChatStats: Decodable, Hashable { + var unreadCount: Int = 0 + var unreadMentions: Int = 0 + var reportsCount: Int = 0 + var minUnreadItemId: Int64 = 0 + var unreadChat: Bool = false +} +``` + +### Computed Properties + +| Property | Description | Line | +|----------|-------------|------| +| `id` | Chat ID from `chatInfo.id` | [L1287](../Shared/Model/ChatModel.swift#L1306) | +| `viewId` | Unique view identity including creation time | [L1289](../Shared/Model/ChatModel.swift#L1308) | +| `unreadTag` | Whether chat counts as "unread" based on notification settings | [L1279](../Shared/Model/ChatModel.swift#L1298) | +| `supportUnreadCount` | Unread count for group support scope | [L1291](../Shared/Model/ChatModel.swift#L1310) | + +--- + +## 6. [ChatInfo](../SimpleXChat/ChatTypes.swift#L1372-L1852) + +**Enum**: `public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable` +**Source**: [`SimpleXChat/ChatTypes.swift`](../SimpleXChat/ChatTypes.swift#L1372) + +Represents the type and metadata of a conversation: + +```swift +public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { + case direct(contact: Contact) + case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?) + case local(noteFolder: NoteFolder) + case contactRequest(contactRequest: UserContactRequest) + case contactConnection(contactConnection: PendingContactConnection) + case invalidJSON(json: Data?) +} +``` + +### Cases + +| Case | Associated Value | Description | +|------|-----------------|-------------| +| `.direct` | `Contact` | One-to-one conversation | +| `.group` | `GroupInfo, GroupChatScopeInfo?` | Group conversation (optional scope for member support threads) | +| `.local` | `NoteFolder` | Local notes (self-chat) | +| `.contactRequest` | `UserContactRequest` | Incoming contact request | +| `.contactConnection` | `PendingContactConnection` | Pending connection | +| `.invalidJSON` | `Data?` | Undecodable chat data | + +### Key Computed Properties on ChatInfo + +| Property | Type | Description | +|----------|------|-------------| +| `chatType` | `ChatType` | `.direct`, `.group`, `.local`, `.contactRequest`, `.contactConnection` | +| `id` | `ChatId` | Prefixed ID (e.g., `"@1"` for direct, `"#5"` for group) | +| `displayName` | `String` | Contact/group name | +| `image` | `String?` | Profile image (base64) | +| `chatSettings` | `ChatSettings?` | Notification/favorite settings | +| `chatTags` | `[Int64]?` | Assigned tag IDs | + +--- + +## 7. State Flow + +### App Start +``` +SimpleXApp.init() + → haskell_init() + → initChatAndMigrate() + → chat_migrate_init_key() -- creates/opens DB + → startChat(mainApp: true) -- starts core + → apiGetChats(userId) -- populates ChatModel.chats + → UI renders ChatListView +``` + +### Opening a Chat +``` +User taps chat in ChatListView + → ItemsModel.loadOpenChat(chatId) + → 250ms delay for navigation animation + → ChatModel.chatId = chatId + → loadChat(chatId:, im:) + → apiGetChat(chatId, pagination: .last(count: 50)) + → ItemsModel.reversedChatItems = [ChatItem] + → ChatView renders messages +``` + +### Receiving a Message (Event) +``` +Haskell core generates ChatEvent.newChatItems + → Event loop calls chat_recv_msg_wait + → Decoded as ChatEvent.newChatItems(user, chatItems) + → ChatModel updates: + 1. Insert new Chat items into ChatModel.chats (preview) + 2. If chat is open: insert into ItemsModel.reversedChatItems + 3. Update ChatStats (unread counts) + 4. Update ChatTagsModel (tag unread counts) + → SwiftUI re-renders affected views via @Published observation +``` + +### Sending a Message +``` +User taps send in ComposeView + → apiSendMessages(type, id, scope, live, ttl, composedMessages) + → Haskell processes, returns ChatResponse1.newChatItems + → ChatModel.chats updated with new preview + → ItemsModel.reversedChatItems gets new item + → ChatView scrolls to bottom, shows sent message +``` + +--- + +## 8. Preference Storage + +### UserDefaults (via @AppStorage) + +App-level UI settings stored in `UserDefaults.standard`: + +| Key Constant | Type | Description | +|--------------|------|-------------| +| `DEFAULT_PERFORM_LA` | `Bool` | Enable local authentication | +| `DEFAULT_PRIVACY_PROTECT_SCREEN` | `Bool` | Hide screen in app switcher | +| `DEFAULT_SHOW_LA_NOTICE` | `Bool` | Show LA setup notice | +| `DEFAULT_NOTIFICATION_ALERT_SHOWN` | `Bool` | Notification permission alert shown | +| `DEFAULT_CALL_KIT_CALLS_IN_RECENTS` | `Bool` | Show CallKit calls in recents | + +### GroupDefaults + +Settings shared between main app and extensions (NSE, SE) via app group `UserDefaults`: + +| Key | Description | +|-----|-------------| +| `appStateGroupDefault` | Current app state (.active/.suspended/.stopped) | +| `dbContainerGroupDefault` | Database container location (.group/.documents) | +| `ntfPreviewModeGroupDefault` | Notification preview mode | +| `storeDBPassphraseGroupDefault` | Whether to store DB passphrase | +| `callKitEnabledGroupDefault` | Whether CallKit is enabled | +| `onboardingStageDefault` | Current onboarding stage | +| `currentThemeDefault` | Current theme name | +| `systemDarkThemeDefault` | Dark mode theme name | +| `themeOverridesDefault` | Custom theme overrides | +| `currentThemeIdsDefault` | Active theme override IDs | + +### Keychain (KeyChain wrapper) + +Sensitive data stored in iOS Keychain: + +| Key | Description | +|-----|-------------| +| `kcDatabasePassword` | SQLite database encryption key | +| `kcAppPassword` | App lock password | +| `kcSelfDestructPassword` | Self-destruct trigger password | + +### Haskell DB (via apiSaveSettings / apiGetSettings) + +Chat-level preferences stored in the SQLite database (managed by Haskell core): + +- Per-contact preferences (timed messages, voice, calls, etc.) +- Per-group preferences +- Per-user notification settings +- Network configuration +- Server lists + +--- + +## Source Files + +| File | Path | +|------|------| +| ChatModel, ItemsModel, Chat, ChatTagsModel | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift) | +| ChatInfo, User, Contact, GroupInfo, ChatItem | [`SimpleXChat/ChatTypes.swift`](../SimpleXChat/ChatTypes.swift) | +| ActiveFilter | [`Shared/Views/ChatList/ChatListView.swift`](../Shared/Views/ChatList/ChatListView.swift#L52) | +| Preference defaults | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift), [`SimpleXChat/FileUtils.swift`](../SimpleXChat/FileUtils.swift) | diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 2700711773..d4b4dfd949 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -910,7 +910,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "ลบข้อความ?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "ลบข้อความ"; /* No comment provided by engineer. */ @@ -1815,7 +1816,7 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "บทบาทของสมาชิกจะถูกเปลี่ยนเป็น \"%@\" สมาชิกจะได้รับคำเชิญใหม่"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!"; /* No comment provided by engineer. */ @@ -2332,13 +2333,13 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "เซิร์ฟเวอร์รีเลย์ปกป้องที่อยู่ IP ของคุณ แต่สามารถสังเกตระยะเวลาของการโทรได้"; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "ลบ"; /* No comment provided by engineer. */ "Remove member" = "ลบสมาชิกออก"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "ลบสมาชิกออก?"; /* No comment provided by engineer. */ diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 9acf2cc425..5cccb67170 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1733,7 +1733,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Mesaj silinsin mi?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Mesajları sil"; /* No comment provided by engineer. */ @@ -3293,10 +3294,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "Üye sohbetten kaldırılacak - bu geri alınamaz!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Üye gruptan çıkarılacaktır - bu geri alınamaz!"; /* alert message */ @@ -4362,7 +4363,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Sil"; /* No comment provided by engineer. */ @@ -4377,7 +4378,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Kişiyi sil"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Kişi silinsin mi?"; /* No comment provided by engineer. */ diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index fe8cfe22a0..305e64fbcf 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -1718,7 +1718,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "Видалити повідомлення?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "Видалити повідомлення"; /* No comment provided by engineer. */ @@ -3263,10 +3264,10 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль учасника буде змінено на \"%@\". Учасник отримає нове запрошення."; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "Учасника буде видалено з чату – це неможливо скасувати!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "Учасник буде видалений з групи - це неможливо скасувати!"; /* alert message */ @@ -4317,7 +4318,7 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Сервер ретрансляції захищає вашу IP-адресу, але він може спостерігати за тривалістю дзвінка."; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "Видалити"; /* No comment provided by engineer. */ @@ -4329,7 +4330,7 @@ swipe action */ /* No comment provided by engineer. */ "Remove member" = "Видалити учасника"; -/* No comment provided by engineer. */ +/* alert title */ "Remove member?" = "Видалити учасника?"; /* No comment provided by engineer. */ diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 24d153afd5..ff80559fb1 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -346,12 +346,21 @@ alert action swipe action */ "Accept" = "接受"; +/* alert action */ +"Accept as member" = "接受为成员"; + +/* alert action */ +"Accept as observer" = "接受为观察员"; + /* No comment provided by engineer. */ "Accept conditions" = "接受条款"; /* No comment provided by engineer. */ "Accept connection request?" = "接受联系人?"; +/* alert title */ +"Accept contact request" = "接受联络请求"; + /* notification body */ "Accept contact request from %@?" = "接受来自 %@ 的联系人请求?"; @@ -359,12 +368,21 @@ swipe action */ swipe action */ "Accept incognito" = "接受隐身聊天"; +/* alert title */ +"Accept member" = "接受成员"; + /* call status */ "accepted call" = "已接受通话"; /* No comment provided by engineer. */ "Accepted conditions" = "已接受的条款"; +/* chat list item title */ +"accepted invitation" = "已接受邀请"; + +/* rcv group event chat item */ +"accepted you" = "接受了你"; + /* No comment provided by engineer. */ "Acknowledged" = "确认"; @@ -386,6 +404,9 @@ swipe action */ /* No comment provided by engineer. */ "Add list" = "添加列表"; +/* placeholder for sending contact request */ +"Add message" = "添加信息"; + /* No comment provided by engineer. */ "Add profile" = "添加个人资料"; @@ -461,6 +482,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "同意加密…"; +/* member criteria value */ +"all" = "全部"; + /* No comment provided by engineer. */ "All" = "全部"; @@ -530,6 +554,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow downgrade" = "允许降级"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "只有你的联系人允许的情况下才允许文件和媒体。"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "仅有您的联系人许可后才允许不可撤回消息移除"; @@ -581,6 +608,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "允许您的联系人发送限时消息。"; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "允许你的联系人发送文件和媒体。"; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "允许您的联系人发送语音消息。"; @@ -683,6 +713,9 @@ swipe action */ /* No comment provided by engineer. */ "Archived contacts" = "已存档的联系人"; +/* No comment provided by engineer. */ +"archived report" = "已存档的举报"; + /* No comment provided by engineer. */ "Archiving database" = "正在存档数据库"; @@ -782,6 +815,12 @@ swipe action */ /* No comment provided by engineer. */ "Better user experience" = "更佳的使用体验"; +/* No comment provided by engineer. */ +"Bio" = "自我介绍"; + +/* alert title */ +"Bio too large" = "自我介绍过大"; + /* No comment provided by engineer. */ "Black" = "黑色"; @@ -825,6 +864,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "bold" = "加粗"; +/* No comment provided by engineer. */ +"Bot" = "机器人"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "您和您的联系人都可以添加消息回应。"; @@ -837,6 +879,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "您和您的联系人都可以发送限时消息。"; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "你和你的联系人都可发送文件和媒体。"; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "您和您的联系人都可以发送语音消息。"; @@ -849,6 +894,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Business chats" = "企业聊天"; +/* No comment provided by engineer. */ +"Business connection" = "企业连接"; + /* No comment provided by engineer. */ "Businesses" = "企业"; @@ -888,6 +936,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't call member" = "无法呼叫成员"; +/* alert title */ +"Can't change profile" = "无法更改个人资料"; + /* No comment provided by engineer. */ "Can't invite contact!" = "无法邀请联系人!"; @@ -897,6 +948,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't message member" = "无法向成员发送消息"; +/* No comment provided by engineer. */ +"can't send messages" = "无法发送消息"; + /* alert action alert button new chat action */ @@ -1035,9 +1089,21 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "将为你删除聊天 - 此操作无法撤销!"; +/* chat toolbar */ +"Chat with admins" = "和管理员聊天"; + +/* No comment provided by engineer. */ +"Chat with member" = "和成员聊天"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "在成员加入前和这些人聊天"; + /* No comment provided by engineer. */ "Chats" = "聊天"; +/* No comment provided by engineer. */ +"Chats with members" = "和成员聊天"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "每 20 分钟检查消息。"; @@ -1179,6 +1245,9 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "自动连接"; +/* No comment provided by engineer. */ +"Connect faster! 🚀" = "更快地连接!🚀"; + /* No comment provided by engineer. */ "Connect to desktop" = "连接到桌面"; @@ -1317,9 +1386,15 @@ set passcode view */ /* No comment provided by engineer. */ "Contact already exists" = "联系人已存在"; +/* No comment provided by engineer. */ +"contact deleted" = "删除了联系人"; + /* No comment provided by engineer. */ "Contact deleted!" = "联系人已删除!"; +/* No comment provided by engineer. */ +"contact disabled" = "禁用了联系人"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "联系人具有端到端加密"; @@ -1338,9 +1413,18 @@ set passcode view */ /* No comment provided by engineer. */ "Contact name" = "联系人姓名"; +/* No comment provided by engineer. */ +"contact not ready" = "联系人未就绪"; + /* No comment provided by engineer. */ "Contact preferences" = "联系人偏好设置"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "来自群的联络请求"; + +/* No comment provided by engineer. */ +"contact should accept…" = "联系人应当接受…"; + /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "联系人将被删除-这是无法撤消的!"; @@ -1410,6 +1494,9 @@ set passcode view */ /* No comment provided by engineer. */ "Create SimpleX address" = "创建 SimpleX 地址"; +/* No comment provided by engineer. */ +"Create your address" = "创建地址"; + /* No comment provided by engineer. */ "Create your profile" = "创建您的资料"; @@ -1583,6 +1670,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat profile?" = "删除聊天资料?"; +/* alert title */ +"Delete chat with member?" = "删除和成员的聊天吗?"; + /* No comment provided by engineer. */ "Delete chat?" = "删除聊天?"; @@ -1640,7 +1730,8 @@ swipe action */ /* No comment provided by engineer. */ "Delete message?" = "删除消息吗?"; -/* alert button */ +/* alert action +alert button */ "Delete messages" = "删除消息"; /* No comment provided by engineer. */ @@ -1709,9 +1800,15 @@ swipe action */ /* No comment provided by engineer. */ "Delivery receipts!" = "送达回执!"; +/* No comment provided by engineer. */ +"Deprecated options" = "已废弃的选项"; + /* No comment provided by engineer. */ "Description" = "描述"; +/* alert title */ +"Description too large" = "描述过大"; + /* No comment provided by engineer. */ "Desktop address" = "桌面地址"; @@ -1914,6 +2011,9 @@ chat item action */ /* No comment provided by engineer. */ "Edit group profile" = "编辑群组资料"; +/* No comment provided by engineer. */ +"Empty message!" = "空消息!"; + /* No comment provided by engineer. */ "Enable" = "启用"; @@ -1926,6 +2026,9 @@ chat item action */ /* No comment provided by engineer. */ "Enable camera access" = "启用相机访问"; +/* No comment provided by engineer. */ +"Enable disappearing messages by default." = "默认启用定时消失消息。"; + /* No comment provided by engineer. */ "Enable Flux in Network & servers settings for better metadata privacy." = "在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。"; @@ -2097,15 +2200,24 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "接受联系人请求错误"; +/* alert title */ +"Error accepting member" = "接受成员出错"; + /* No comment provided by engineer. */ "Error adding member(s)" = "添加成员错误"; /* alert title */ "Error adding server" = "添加服务器出错"; +/* No comment provided by engineer. */ +"Error adding short link" = "添加短链接出错"; + /* No comment provided by engineer. */ "Error changing address" = "更改地址错误"; +/* alert title */ +"Error changing chat profile" = "更改聊天资料出错"; + /* No comment provided by engineer. */ "Error changing connection profile" = "更改连接资料出错"; @@ -2118,6 +2230,9 @@ chat item action */ /* No comment provided by engineer. */ "Error changing to incognito!" = "切换至隐身聊天出错!"; +/* No comment provided by engineer. */ +"Error checking token status" = "查询token状态出错"; + /* alert message */ "Error connecting to forwarding server %@. Please try later." = "连接到转发服务器 %@ 时出错。请稍后尝试。"; @@ -2148,6 +2263,9 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "解密文件时出错"; +/* alert title */ +"Error deleting chat" = "删除聊天出错"; + /* alert title */ "Error deleting chat database" = "删除聊天数据库错误"; @@ -2202,6 +2320,9 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "打开聊天时出错"; +/* No comment provided by engineer. */ +"Error opening group" = "打开群时出错"; + /* alert title */ "Error receiving file" = "接收文件错误"; @@ -2214,6 +2335,9 @@ chat item action */ /* alert title */ "Error registering for notifications" = "注册消息推送出错"; +/* alert title */ +"Error rejecting contact request" = "拒绝联络请求出错"; + /* alert title */ "Error removing member" = "删除成员错误"; @@ -2259,6 +2383,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "发送消息错误"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "设置自动接受出错"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "设置送达回执出错!"; @@ -2309,6 +2436,9 @@ file error text snd error text */ "Error: %@" = "错误: %@"; +/* server test error */ +"Error: %@." = "错误:%@。"; + /* No comment provided by engineer. */ "Error: no database file" = "错误:没有数据库文件"; @@ -2417,6 +2547,9 @@ snd error text */ /* chat feature */ "Files and media" = "文件和媒体"; +/* No comment provided by engineer. */ +"Files and media are prohibited in this chat." = "此聊天禁止文件和媒体。"; + /* No comment provided by engineer. */ "Files and media are prohibited." = "此群组中禁止文件和媒体。"; @@ -2441,6 +2574,15 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "更快地查找聊天记录"; +/* No comment provided by engineer. */ +"Fingerprint in destination server address does not match certificate: %@." = "目的地服务器的指纹与证书不符:%@。"; + +/* No comment provided by engineer. */ +"Fingerprint in forwarding server address does not match certificate: %@." = "转发服务器的指纹与证书不符:%@。"; + +/* No comment provided by engineer. */ +"Fingerprint in server address does not match certificate: %@." = "服务器的指纹与证书不符:%@。"; + /* server test error */ "Fingerprint in server address does not match certificate." = "服务器地址中的证书指纹可能不正确"; @@ -2561,6 +2703,9 @@ snd error text */ /* message preview */ "Good morning!" = "早上好!"; +/* shown on group welcome message */ +"group" = "群"; + /* No comment provided by engineer. */ "Group" = "群组"; @@ -2591,6 +2736,9 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "群组邀请不再有效,已被发件人删除。"; +/* No comment provided by engineer. */ +"group is deleted" = "群被删除了"; + /* No comment provided by engineer. */ "Group link" = "群组链接"; @@ -2615,6 +2763,9 @@ snd error text */ /* snd group event chat item */ "group profile updated" = "群组资料已更新"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "群资料已修改。如果你进行保存,修改后的群资料将发送给其他群成员。"; + /* No comment provided by engineer. */ "Group welcome message" = "群欢迎词"; @@ -2990,6 +3141,9 @@ snd error text */ /* alert title */ "Keep unused invitation?" = "保留未使用的邀请吗?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "保持聊天洁净"; + /* No comment provided by engineer. */ "Keep your connections" = "保持连接"; @@ -3023,6 +3177,9 @@ snd error text */ /* rcv group event chat item */ "left" = "已离开"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "消耗更少的移动网络数据。"; + /* email subject */ "Let's talk in SimpleX Chat" = "让我们一起在 SimpleX Chat 里聊天"; @@ -3059,6 +3216,9 @@ snd error text */ /* No comment provided by engineer. */ "Live messages" = "实时消息"; +/* in progress text */ +"Loading profile…" = "正加载个人资料…"; + /* No comment provided by engineer. */ "Local name" = "本地名称"; @@ -3110,15 +3270,27 @@ snd error text */ /* No comment provided by engineer. */ "Member" = "成员"; +/* past/unknown group member */ +"Member %@" = "成员 %@"; + /* profile update event chat item */ "member %@ changed to %@" = "成员 %1$@ 已更改为 %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "成员准入"; + /* rcv group event chat item */ "member connected" = "已连接"; +/* No comment provided by engineer. */ +"member has old version" = "成员有旧版本"; + /* item status text */ "Member inactive" = "成员不活跃"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "成员被删除——无法接受请求"; + /* chat feature */ "Member reports" = "成员举报"; @@ -3131,12 +3303,15 @@ snd error text */ /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "成员角色将更改为 \"%@\"。该成员将收到一份新的邀请。"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from chat - this cannot be undone!" = "将从聊天中删除成员 - 此操作无法撤销!"; -/* No comment provided by engineer. */ +/* alert message */ "Member will be removed from group - this cannot be undone!" = "成员将被移出群组——此操作无法撤消!"; +/* alert message */ +"Member will join the group, accept member?" = "成员将加入本群,接受成员吗?"; + /* No comment provided by engineer. */ "Members can add message reactions." = "群组成员可以添加信息回应。"; @@ -3185,6 +3360,9 @@ snd error text */ /* item status text */ "Message forwarded" = "消息已转发"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "轻按连接后即刻发消息。"; + /* item status description */ "Message may be delivered later if member becomes active." = "如果 member 变为活动状态,则稍后可能会发送消息。"; @@ -3233,6 +3411,9 @@ snd error text */ /* No comment provided by engineer. */ "Messages & files" = "消息"; +/* No comment provided by engineer. */ +"Messages are protected by **end-to-end encryption**." = "消息已通过**端到端加密**保护。"; + /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "将显示来自 %@ 的消息!"; @@ -3311,6 +3492,9 @@ snd error text */ /* marked deleted chat item preview text */ "moderated by %@" = "由 %@ 审核"; +/* member role */ +"moderator" = "协管"; + /* time unit */ "months" = "月"; @@ -3395,6 +3579,9 @@ snd error text */ /* notification */ "New events" = "新事件"; +/* No comment provided by engineer. */ +"New group role: Moderator" = "新的群角色:协管"; + /* No comment provided by engineer. */ "New in %@" = "%@ 的新内容"; @@ -3404,6 +3591,9 @@ snd error text */ /* No comment provided by engineer. */ "New member role" = "新成员角色"; +/* rcv group event chat item */ +"New member wants to join the group." = "新成员要加入本群。"; + /* notification */ "new message" = "新消息"; @@ -3443,6 +3633,9 @@ snd error text */ /* No comment provided by engineer. */ "No chats in list %@" = "列表 %@ 中无聊天"; +/* No comment provided by engineer. */ +"No chats with members" = "没有和成员的聊天"; + /* No comment provided by engineer. */ "No contacts selected" = "未选择联系人"; @@ -3494,6 +3687,9 @@ snd error text */ /* No comment provided by engineer. */ "No permission to record voice message" = "没有录制语音消息的权限"; +/* alert title */ +"No private routing session" = "无私密路由会话"; + /* No comment provided by engineer. */ "No push server" = "本地"; @@ -3512,6 +3708,9 @@ snd error text */ /* servers error */ "No servers to send files." = "无文件发送服务器。"; +/* No comment provided by engineer. */ +"no subscription" = "无订阅"; + /* copied message info in history */ "no text" = "无文本"; @@ -3527,6 +3726,9 @@ snd error text */ /* No comment provided by engineer. */ "Not compatible!" = "不兼容!"; +/* No comment provided by engineer. */ +"not synchronized" = "未同步"; + /* No comment provided by engineer. */ "Notes" = "附注"; @@ -3634,6 +3836,9 @@ new chat action */ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "只有您可以发送限时消息。"; +/* No comment provided by engineer. */ +"Only you can send files and media." = "只有你可以发送文件和媒体。"; + /* No comment provided by engineer. */ "Only you can send voice messages." = "只有您可以发送语音消息。"; @@ -3649,6 +3854,9 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send disappearing messages." = "只有您的联系人才可以发送限时消息。"; +/* No comment provided by engineer. */ +"Only your contact can send files and media." = "只有你的联系人可以发送文件和媒体。"; + /* No comment provided by engineer. */ "Only your contact can send voice messages." = "只有您的联系人可以发送语音消息。"; @@ -3664,18 +3872,45 @@ new chat action */ /* authentication reason */ "Open chat console" = "打开聊天控制台"; +/* alert action */ +"Open clean link" = "打开干净链接"; + /* No comment provided by engineer. */ "Open conditions" = "打开条款"; +/* alert action */ +"Open full link" = "打开完整链接"; + /* new chat action */ "Open group" = "打开群"; +/* alert title */ +"Open link?" = "打开链接?"; + /* authentication reason */ "Open migration to another device" = "打开迁移到另一台设备"; +/* new chat action */ +"Open new chat" = "打开新聊天"; + +/* new chat action */ +"Open new group" = "打开新群"; + /* No comment provided by engineer. */ "Open Settings" = "打开设置"; +/* No comment provided by engineer. */ +"Open to accept" = "打开以接受"; + +/* No comment provided by engineer. */ +"Open to connect" = "打开以连接"; + +/* No comment provided by engineer. */ +"Open to join" = "打开以加入"; + +/* No comment provided by engineer. */ +"Open to use bot" = "打开来使用机器人"; + /* No comment provided by engineer. */ "Opening app…" = "正在打开应用程序…"; @@ -3715,6 +3950,9 @@ new chat action */ /* No comment provided by engineer. */ "other errors" = "其他错误"; +/* alert message */ +"Other file errors:\n%@" = "其他文件错误:\n%@"; + /* member role */ "owner" = "群主"; @@ -3760,6 +3998,12 @@ new chat action */ /* No comment provided by engineer. */ "Pending" = "待定"; +/* No comment provided by engineer. */ +"pending approval" = "待批准"; + +/* No comment provided by engineer. */ +"pending review" = "待审核"; + /* No comment provided by engineer. */ "Periodic" = "定期"; @@ -3826,15 +4070,33 @@ new chat action */ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "请安全地保存密码,如果您丢失了密码,您将无法更改它。"; +/* token info */ +"Please try to disable and re-enable notfications." = "请尝试禁用并重新启用通知。"; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "请等待群的协管审核你加入该群的请求。"; + +/* token info */ +"Please wait for token activation to complete." = "请等待token激活完成。"; + +/* token info */ +"Please wait for token to be registered." = "请等待token注册完成。"; + /* No comment provided by engineer. */ "Polish interface" = "波兰语界面"; +/* No comment provided by engineer. */ +"Port" = "端口"; + /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "保留最后的消息草稿及其附件。"; /* No comment provided by engineer. */ "Preset server address" = "预设服务器地址"; +/* No comment provided by engineer. */ +"Preset servers" = "预设服务器"; + /* No comment provided by engineer. */ "Preview" = "预览"; @@ -3844,6 +4106,9 @@ new chat action */ /* No comment provided by engineer. */ "Privacy & security" = "隐私和安全"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "客户隐私。"; + /* No comment provided by engineer. */ "Privacy policy and conditions of use." = "隐私政策和使用条款。"; @@ -3856,6 +4121,9 @@ new chat action */ /* No comment provided by engineer. */ "Private filenames" = "私密文件名"; +/* No comment provided by engineer. */ +"Private media file names." = "私密媒体文件名。"; + /* No comment provided by engineer. */ "Private message routing" = "私有消息路由"; @@ -3871,6 +4139,9 @@ new chat action */ /* alert title */ "Private routing error" = "专用路由错误"; +/* alert title */ +"Private routing timeout" = "私密路由超时"; + /* No comment provided by engineer. */ "Profile and server connections" = "资料和服务器连接"; @@ -3901,6 +4172,9 @@ new chat action */ /* No comment provided by engineer. */ "Prohibit messages reactions." = "禁止消息回应。"; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "禁止向 协管 举报消息。"; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "禁止向成员发送私信。"; @@ -3928,6 +4202,9 @@ new chat action */ /* No comment provided by engineer. */ "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "保护您的 IP 地址免受联系人选择的消息中继的攻击。\n在*网络和服务器*设置中启用。"; +/* No comment provided by engineer. */ +"Protocol background timeout" = "协议后台超时"; + /* No comment provided by engineer. */ "Protocol timeout" = "协议超时"; @@ -3940,6 +4217,9 @@ new chat action */ /* No comment provided by engineer. */ "Proxied servers" = "代理服务器"; +/* No comment provided by engineer. */ +"Proxy requires password" = "代理需要密码"; + /* No comment provided by engineer. */ "Push notifications" = "推送通知"; @@ -4057,6 +4337,12 @@ new chat action */ /* No comment provided by engineer. */ "Reduced battery usage" = "减少电池使用量"; +/* No comment provided by engineer. */ +"Register" = "注册"; + +/* token status text */ +"Registered" = "已注册"; + /* alert action reject incoming call via notification swipe action */ @@ -4068,6 +4354,12 @@ swipe action */ /* alert title */ "Reject contact request" = "拒绝联系人请求"; +/* alert title */ +"Reject member?" = "拒绝成员?"; + +/* No comment provided by engineer. */ +"rejected" = "被拒绝"; + /* call status */ "rejected call" = "拒接来电"; @@ -4077,16 +4369,22 @@ swipe action */ /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "中继服务器保护您的 IP 地址,但它可以观察通话的持续时间。"; -/* No comment provided by engineer. */ +/* alert action */ "Remove" = "移除"; +/* No comment provided by engineer. */ +"Remove archive?" = "删除存档?"; + /* No comment provided by engineer. */ "Remove image" = "移除图片"; /* No comment provided by engineer. */ -"Remove member" = "删除成员"; +"Remove link tracking" = "删除链接跟踪"; /* No comment provided by engineer. */ +"Remove member" = "删除成员"; + +/* alert title */ "Remove member?" = "删除成员吗?"; /* No comment provided by engineer. */ @@ -4101,12 +4399,18 @@ swipe action */ /* profile update event chat item */ "removed contact address" = "删除了联系地址"; +/* No comment provided by engineer. */ +"removed from group" = "从群被删除了"; + /* profile update event chat item */ "removed profile picture" = "删除了资料图片"; /* rcv group event chat item */ "removed you" = "已将您移除"; +/* No comment provided by engineer. */ +"Removes messages and blocks members." = "删除消息并封禁成员。"; + /* No comment provided by engineer. */ "Renegotiate" = "重新协商"; @@ -4128,6 +4432,54 @@ swipe action */ /* chat item action */ "Reply" = "回复"; +/* chat item action */ +"Report" = "举报"; + +/* report reason */ +"Report content: only group moderators will see it." = "举报内容:仅协管会看到。"; + +/* report reason */ +"Report member profile: only group moderators will see it." = "举报成员个人资料:仅协管会看到。"; + +/* report reason */ +"Report other: only group moderators will see it." = "举报其他:仅协管会看到。"; + +/* No comment provided by engineer. */ +"Report reason?" = "举报理由?"; + +/* alert title */ +"Report sent to moderators" = "举报已发送至 协管"; + +/* report reason */ +"Report spam: only group moderators will see it." = "举报垃圾信息:仅协管会看到。"; + +/* report reason */ +"Report violation: only group moderators will see it." = "举报违规:仅协管会看到。"; + +/* report in notification */ +"Report: %@" = "举报: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "向协管举报消息已被禁止。"; + +/* No comment provided by engineer. */ +"Reports" = "举报"; + +/* No comment provided by engineer. */ +"request is sent" = "发送了请求"; + +/* No comment provided by engineer. */ +"request to join rejected" = "加入请求被拒绝"; + +/* rcv group event chat item */ +"requested connection" = "已请求连接"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "来自群组%@的已请求连接"; + +/* chat list item title */ +"requested to connect" = "被请求连接"; + /* No comment provided by engineer. */ "Required" = "必须"; @@ -4179,9 +4531,21 @@ swipe action */ /* chat item action */ "Reveal" = "揭示"; +/* No comment provided by engineer. */ +"review" = "审核"; + /* No comment provided by engineer. */ "Review conditions" = "审阅条款"; +/* No comment provided by engineer. */ +"Review group members" = "审核群成员"; + +/* admission stage */ +"Review members" = "审核成员"; + +/* No comment provided by engineer. */ +"reviewed by admins" = "由管理员审核"; + /* No comment provided by engineer. */ "Revoke" = "吊销"; @@ -4210,6 +4574,12 @@ chat item action */ /* alert button */ "Save (and notify contacts)" = "保存(并通知联系人)"; +/* alert button */ +"Save (and notify members)" = "保存(并通知成员)"; + +/* alert title */ +"Save admission settings?" = "保存入群设置?"; + /* alert button */ "Save and notify contact" = "保存并通知联系人"; @@ -4225,6 +4595,9 @@ chat item action */ /* No comment provided by engineer. */ "Save group profile" = "保存群组资料"; +/* alert title */ +"Save group profile?" = "保存群资料?"; + /* No comment provided by engineer. */ "Save list" = "保存列表"; @@ -4336,6 +4709,9 @@ chat item action */ /* chat item action */ "Select" = "选择"; +/* No comment provided by engineer. */ +"Select chat profile" = "选择聊天个人资料"; + /* No comment provided by engineer. */ "Selected %lld" = "选定的 %lld"; @@ -4360,6 +4736,9 @@ chat item action */ /* No comment provided by engineer. */ "Send a live message - it will update for the recipient(s) as you type it" = "发送实时消息——它会在您键入时为收件人更新"; +/* No comment provided by engineer. */ +"Send contact request?" = "发送联络请求?"; + /* No comment provided by engineer. */ "Send delivery receipts to" = "将送达回执发送给"; @@ -4390,18 +4769,30 @@ chat item action */ /* No comment provided by engineer. */ "Send notifications" = "发送通知"; +/* No comment provided by engineer. */ +"Send private reports" = "发送私下举报"; + /* No comment provided by engineer. */ "Send questions and ideas" = "发送问题和想法"; /* No comment provided by engineer. */ "Send receipts" = "发送回执"; +/* No comment provided by engineer. */ +"Send request" = "发送请求"; + +/* No comment provided by engineer. */ +"Send request without message" = "发送无消息请求"; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "发送它们来自图库或自定义键盘。"; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "给新成员发送最多 100 条历史消息。"; +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "向群发送私密反馈。"; + /* alert message */ "Sender cancelled file transfer." = "发送人已取消文件传输。"; @@ -4459,6 +4850,12 @@ chat item action */ /* No comment provided by engineer. */ "Sent via proxy" = "通过代理发送"; +/* No comment provided by engineer. */ +"Server" = "服务器"; + +/* alert message */ +"Server added to operator %@." = "服务器已添加到运营方 %@。"; + /* No comment provided by engineer. */ "Server address" = "服务器地址"; @@ -4468,6 +4865,15 @@ chat item action */ /* srv error text. */ "Server address is incompatible with network settings." = "服务器地址与网络设置不兼容。"; +/* alert title */ +"Server operator changed." = "服务器运营方已更改。"; + +/* No comment provided by engineer. */ +"Server operators" = "服务器运营方"; + +/* alert title */ +"Server protocol changed." = "服务器协议已更改。"; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "服务器队列信息: %1$@\n\n上次收到的消息: %2$@"; @@ -4504,6 +4910,9 @@ chat item action */ /* No comment provided by engineer. */ "Set 1 day" = "设定1天"; +/* No comment provided by engineer. */ +"Set chat name…" = "设置聊天名称…"; + /* No comment provided by engineer. */ "Set contact name…" = "设置联系人姓名……"; @@ -4516,6 +4925,12 @@ chat item action */ /* No comment provided by engineer. */ "Set it instead of system authentication." = "设置它以代替系统身份验证。"; +/* No comment provided by engineer. */ +"Set member admission" = "设置成员入群准许"; + +/* No comment provided by engineer. */ +"Set message expiration in chats." = "在聊天中设置消息过期时间。"; + /* profile update event chat item */ "set new contact address" = "设置新的联系地址"; @@ -4531,6 +4946,9 @@ chat item action */ /* No comment provided by engineer. */ "Set passphrase to export" = "设置密码来导出"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "设置自我介绍和欢迎消息。"; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "设置向新成员显示的消息!"; @@ -4540,6 +4958,9 @@ chat item action */ /* No comment provided by engineer. */ "Settings" = "设置"; +/* alert message */ +"Settings were changed." = "设置已修改。"; + /* No comment provided by engineer. */ "Shape profile images" = "改变个人资料图形状"; @@ -4550,9 +4971,15 @@ chat item action */ /* No comment provided by engineer. */ "Share 1-time link" = "分享一次性链接"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "和一位好友分享一次性链接"; + /* No comment provided by engineer. */ "Share address" = "分享地址"; +/* No comment provided by engineer. */ +"Share address publicly" = "公开分享地址"; + /* alert title */ "Share address with contacts?" = "与联系人分享地址?"; @@ -4562,6 +4989,18 @@ chat item action */ /* No comment provided by engineer. */ "Share link" = "分享链接"; +/* alert button */ +"Share old address" = "分享旧地址"; + +/* alert button */ +"Share old link" = "分享旧链接"; + +/* No comment provided by engineer. */ +"Share profile" = "分享资料"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "在社媒上分享 SimpleX 地址。"; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "分享此一次性邀请链接"; @@ -4571,6 +5010,18 @@ chat item action */ /* No comment provided by engineer. */ "Share with contacts" = "与联系人分享"; +/* No comment provided by engineer. */ +"Share your address" = "分享地址"; + +/* No comment provided by engineer. */ +"Short description" = "短描述"; + +/* No comment provided by engineer. */ +"Short link" = "短链接"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "SimpleX 短地址"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "显示 → 通过专用路由发送的信息."; @@ -4661,6 +5112,9 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "SimpleX 协议由 Trail of Bits 审阅。"; +/* simplex link type */ +"SimpleX relay link" = "SimpleX 中继链接"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "简化的隐身模式"; @@ -4679,6 +5133,9 @@ chat item action */ /* No comment provided by engineer. */ "SMP server" = "SMP 服务器"; +/* No comment provided by engineer. */ +"SOCKS proxy" = "SOCKS代理"; + /* blur media */ "Soft" = "软"; @@ -4694,9 +5151,16 @@ chat item action */ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "导入过程中出现一些非致命错误:"; +/* alert message */ +"Some servers failed the test:\n%@" = "有服务器测试未通过:\n%@"; + /* notification title */ "Somebody" = "某人"; +/* blocking reason +report reason */ +"Spam" = "垃圾信息"; + /* No comment provided by engineer. */ "Square, circle, or anything in between." = "方形、圆形、或两者之间的任意形状."; @@ -4775,18 +5239,42 @@ chat item action */ /* No comment provided by engineer. */ "Support SimpleX Chat" = "支持 SimpleX Chat"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "通话期间切换音频和视频。"; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "对一次性邀请切换聊天个人资料。"; + /* No comment provided by engineer. */ "System" = "系统"; /* No comment provided by engineer. */ "System authentication" = "系统验证"; +/* No comment provided by engineer. */ +"Tail" = "尾部"; + /* No comment provided by engineer. */ "Take picture" = "拍照"; /* No comment provided by engineer. */ "Tap button " = "点击按钮 "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "轻按连接进行聊天"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "轻按连接来发送请求"; + +/* No comment provided by engineer. */ +"Tap Connect to use bot" = "轻按“连接”使用机器人"; + +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址”"; + +/* No comment provided by engineer. */ +"Tap Join group" = "轻按加入群"; + /* No comment provided by engineer. */ "Tap to activate profile." = "点击以激活个人资料。"; @@ -4808,9 +5296,15 @@ chat item action */ /* No comment provided by engineer. */ "TCP connection" = "TCP 连接"; +/* No comment provided by engineer. */ +"TCP connection bg timeout" = "TCP 连接后台超时"; + /* No comment provided by engineer. */ "TCP connection timeout" = "TCP 连接超时"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "用于消息收发的 TCP 端口"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4826,6 +5320,9 @@ chat item action */ /* server test failure */ "Test failed at step %@." = "在步骤 %@ 上测试失败。"; +/* No comment provided by engineer. */ +"Test notifications" = "测试通知"; + /* No comment provided by engineer. */ "Test server" = "测试服务器"; @@ -4844,9 +5341,15 @@ chat item action */ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "感谢用户——通过 Weblate 做出贡献!"; +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "地址不会长,将通过该简短地址分享个人资料。"; + /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。"; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "应用通过在每个对话中使用不同运营方保护你的隐私。"; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "该应用程序将要求确认从未知文件服务器(.onion 除外)下载。"; @@ -4856,6 +5359,9 @@ chat item action */ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "您扫描的码不是 SimpleX 链接的二维码。"; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "连接达到了未送达消息上限,你的联系人可能处于离线状态。"; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "您接受的连接将被取消!"; @@ -4877,6 +5383,9 @@ chat item action */ /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "下一条消息的 ID 不正确(小于或等于上一条)。\n它可能是由于某些错误或连接被破坏才发生。"; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "链接不会长,群资料会通过短链接分享。"; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "将为所有成员删除该消息。"; @@ -4892,6 +5401,9 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "旧数据库在迁移过程中没有被移除,可以删除。"; +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "应用中的第二个预设运营方!"; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "我们错过的第二个\"√\"!✅"; @@ -4904,9 +5416,15 @@ chat item action */ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "您粘贴的文本不是 SimpleX 链接。"; +/* No comment provided by engineer. */ +"The uploaded database archive will be permanently removed from the servers." = "已上传的数据库归档将会从服务器中永久移除。"; + /* No comment provided by engineer. */ "Themes" = "主题"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "这些条件将同样适用于: **%@**。"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "这些设置适用于您当前的配置文件 **%@**。"; @@ -4919,6 +5437,9 @@ chat item action */ /* No comment provided by engineer. */ "This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "此操作无法撤消——早于所选的发送和接收的消息将被删除。 这可能需要几分钟时间。"; +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "此操作无法撤销 —— 比此聊天中所选消息更早发出并收到的消息将被删除。"; + /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。"; @@ -4943,12 +5464,24 @@ chat item action */ /* No comment provided by engineer. */ "This group no longer exists." = "该群组已不存在。"; +/* No comment provided by engineer. */ +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。"; + /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "此链接已在其他移动设备上使用,请在桌面上创建新链接。"; +/* No comment provided by engineer. */ +"This message was deleted or not received yet." = "此消息被删除或尚未收到。"; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "此设置适用于您当前聊天资料 **%@** 中的消息。"; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "此设置用于当前个人资料 **%@**。"; + +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "只为新联系人设置了消失时间。"; + /* No comment provided by engineer. */ "Title" = "标题"; @@ -4964,6 +5497,9 @@ chat item action */ /* No comment provided by engineer. */ "To make a new connection" = "建立新连接"; +/* No comment provided by engineer. */ +"To protect against your link being replaced, you can compare contact security codes." = "为了防止链接被替换,你可以比较联系人安全代码。"; + /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "为了保护时区,图像/语音文件使用 UTC。"; @@ -4976,15 +5512,36 @@ chat item action */ /* No comment provided by engineer. */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。"; +/* No comment provided by engineer. */ +"To receive" = "消息接收"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "为了记录语音请授予使用麦克风权限。"; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "为了录制视频请授予使用相机权限。"; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "请授权使用麦克风以录制语音消息。"; /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "要显示您的隐藏的个人资料,请在**您的聊天个人资料**页面的搜索字段中输入完整密码。"; +/* No comment provided by engineer. */ +"To send" = "发送"; + +/* alert message */ +"To send commands you must be connected." = "你必须已连接才能发送命令。"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "为了支持即时推送通知,聊天数据库必须被迁移。"; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "要在连接尝试后使用不同的个人资料,请删除聊天并再次使用该链接。"; + +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "要使用**%@**的服务器,需接受条款。"; + /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。"; @@ -5006,6 +5563,9 @@ chat item action */ /* No comment provided by engineer. */ "Transport sessions" = "传输会话"; +/* subscription status explanation */ +"Trying to connect to the server used to receive messages from this connection." = "尝试连接到用于从该连接接收消息的服务器。"; + /* No comment provided by engineer. */ "Turkish interface" = "土耳其语界面"; @@ -5036,6 +5596,9 @@ chat item action */ /* rcv group event chat item */ "unblocked %@" = "未阻止 %@"; +/* No comment provided by engineer. */ +"Undelivered messages" = "未送达的消息"; + /* No comment provided by engineer. */ "Unexpected migration state" = "未预料的迁移状态"; @@ -5102,6 +5665,9 @@ chat item action */ /* swipe action */ "Unread" = "未读"; +/* No comment provided by engineer. */ +"Unsupported connection link" = "不支持的连接链接"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "给新成员发送了最多 100 条历史消息。"; @@ -5117,6 +5683,9 @@ chat item action */ /* No comment provided by engineer. */ "Update settings?" = "更新设置?"; +/* No comment provided by engineer. */ +"Updated conditions" = "条款已更新"; + /* rcv group event chat item */ "updated group profile" = "已更新的群组资料"; @@ -5126,9 +5695,27 @@ chat item action */ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "更新设置会将客户端重新连接到所有服务器。"; +/* alert button */ +"Upgrade" = "升级"; + +/* No comment provided by engineer. */ +"Upgrade address" = "升级地址"; + +/* alert message */ +"Upgrade address?" = "升级地址?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "升级并打开聊天"; +/* alert message */ +"Upgrade group link?" = "升级群链接?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "升级链接"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "升级你的地址"; + /* No comment provided by engineer. */ "Upload errors" = "上传错误"; @@ -5150,18 +5737,30 @@ chat item action */ /* No comment provided by engineer. */ "Use .onion hosts" = "使用 .onion 主机"; +/* No comment provided by engineer. */ +"Use %@" = "使用 %@"; + /* No comment provided by engineer. */ "Use chat" = "使用聊天"; /* new chat action */ "Use current profile" = "使用当前配置文件"; +/* No comment provided by engineer. */ +"Use for files" = "用于文件"; + +/* No comment provided by engineer. */ +"Use for messages" = "用于消息"; + /* No comment provided by engineer. */ "Use for new connections" = "用于新连接"; /* No comment provided by engineer. */ "Use from desktop" = "从桌面端使用"; +/* No comment provided by engineer. */ +"Use incognito profile" = "使用隐身个人资料"; + /* No comment provided by engineer. */ "Use iOS call interface" = "使用 iOS 通话界面"; @@ -5180,18 +5779,36 @@ chat item action */ /* No comment provided by engineer. */ "Use server" = "使用服务器"; +/* No comment provided by engineer. */ +"Use servers" = "使用服务器"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "使用 SimpleX Chat 服务器?"; +/* No comment provided by engineer. */ +"Use SOCKS proxy" = "使用 SOCKS 代理"; + +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "当未指定端口时使用TCP端口%@。"; + +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "仅预设服务器使用 TCP 协议 443 端口。"; + /* No comment provided by engineer. */ "Use the app while in the call." = "通话时使用本应用."; /* No comment provided by engineer. */ "Use the app with one hand." = "用一只手使用应用程序。"; +/* No comment provided by engineer. */ +"Use web port" = "使用 web 端口"; + /* No comment provided by engineer. */ "User selection" = "用户选择"; +/* No comment provided by engineer. */ +"Username" = "用户名"; + /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "使用 SimpleX Chat 服务器。"; @@ -5258,9 +5875,15 @@ chat item action */ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "最大 1gb 的视频和文件"; +/* No comment provided by engineer. */ +"View conditions" = "查看条款"; + /* No comment provided by engineer. */ "View security code" = "查看安全码"; +/* No comment provided by engineer. */ +"View updated conditions" = "查看更新后的条款"; + /* chat feature */ "Visible history" = "可见的历史"; @@ -5330,6 +5953,9 @@ chat item action */ /* No comment provided by engineer. */ "Welcome message is too long" = "欢迎消息太大了"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "欢迎联系人👋"; + /* No comment provided by engineer. */ "What's new" = "更新内容"; @@ -5342,6 +5968,9 @@ chat item action */ /* No comment provided by engineer. */ "when IP hidden" = "当 IP 隐藏时"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "当启用了超过一个运营方时,没有一个运营方拥有了解谁和谁联络的元数据。"; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。"; @@ -5396,6 +6025,9 @@ chat item action */ /* No comment provided by engineer. */ "You accepted connection" = "您已接受连接"; +/* snd group event chat item */ +"you accepted this member" = "你接受了该成员"; + /* No comment provided by engineer. */ "You allow" = "您允许"; @@ -5405,6 +6037,9 @@ chat item action */ /* No comment provided by engineer. */ "You are already connected to %@." = "您已经连接到 %@。"; +/* No comment provided by engineer. */ +"You are already connected with %@." = "你已经与%@保持连接。"; + /* new chat sheet message */ "You are already connecting to %@." = "您已连接到 %@。"; @@ -5423,9 +6058,15 @@ chat item action */ /* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "您已经加入了这个群组!\n重复加入请求?"; +/* subscription status explanation */ +"You are connected to the server used to receive messages from this connection." = "你已连接到用于接收该连接消息的服务器。"; + /* No comment provided by engineer. */ "You are invited to group" = "您被邀请加入群组"; +/* subscription status explanation */ +"You are not connected to the server used to receive messages from this connection (no subscription)." = "未连接到用于从该连接接收消息的服务器(无订阅)。"; + /* No comment provided by engineer. */ "You are not connected to these servers. Private routing is used to deliver messages to them." = "您未连接到这些服务器。私有路由用于向他们发送消息。"; @@ -5441,6 +6082,9 @@ chat item action */ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "您可以在外观设置中更改它。"; +/* No comment provided by engineer. */ +"You can configure servers via settings." = "你可以通过设置配置服务器。"; + /* No comment provided by engineer. */ "You can create it later" = "您可以以后创建它"; @@ -5465,6 +6109,9 @@ chat item action */ /* No comment provided by engineer. */ "You can send messages to %@ from Archived contacts." = "您可以从存档的联系人向%@发送消息。"; +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "你可以设置连接名称,用来记住和谁分享了这个链接。"; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "您可以通过设置来设置锁屏通知预览。"; @@ -5489,6 +6136,9 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "您可以在连接详情中再次查看邀请链接。"; +/* alert message */ +"You can view your reports in Chat with admins." = "你可以在和管理员和聊天中查看你的举报。"; + /* alert title */ "You can't send messages!" = "您无法发送消息!"; @@ -5561,6 +6211,9 @@ chat item action */ /* snd group event chat item */ "you unblocked %@" = "您解封了 %@"; +/* No comment provided by engineer. */ +"You will be able to send messages **only after your request is accepted**." = "**只有在你的请求被接受后**你才能发送消息。"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "您将在组主设备上线时连接到该群组,请稍等或稍后再检查!"; @@ -5579,6 +6232,9 @@ chat item action */ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。"; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "你将停止从这个聊天收到消息。聊天历史将被保留。"; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "您将停止接收来自该群组的消息。聊天记录将被保留。"; @@ -5594,6 +6250,9 @@ chat item action */ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人"; +/* No comment provided by engineer. */ +"Your business contact" = "你的企业联系人"; + /* No comment provided by engineer. */ "Your calls" = "您的通话"; @@ -5603,9 +6262,15 @@ chat item action */ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "您的聊天数据库未加密——设置密码来加密。"; +/* alert title */ +"Your chat preferences" = "你的聊天偏好设置"; + /* No comment provided by engineer. */ "Your chat profiles" = "您的聊天资料"; +/* No comment provided by engineer. */ +"Your contact" = "你的联系人"; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "您的联系人发送的文件大于当前支持的最大大小 (%@)。"; @@ -5615,12 +6280,18 @@ chat item action */ /* No comment provided by engineer. */ "Your contacts will remain connected." = "与您的联系人保持连接。"; +/* No comment provided by engineer. */ +"Your credentials may be sent unencrypted." = "你的凭据可能以未经加密的方式被发送。"; + /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "您当前的聊天数据库将被删除并替换为导入的数据库。"; /* No comment provided by engineer. */ "Your current profile" = "您当前的资料"; +/* No comment provided by engineer. */ +"Your group" = "你的群"; + /* No comment provided by engineer. */ "Your ICE servers" = "您的 ICE 服务器"; @@ -5642,12 +6313,18 @@ chat item action */ /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。"; +/* alert message */ +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "您的个人资料已修改。如果进行保存,更新后的个人资料将发送到所有联系人。"; + /* No comment provided by engineer. */ "Your random profile" = "您的随机资料"; /* No comment provided by engineer. */ "Your server address" = "您的服务器地址"; +/* No comment provided by engineer. */ +"Your servers" = "你的服务器"; + /* No comment provided by engineer. */ "Your settings" = "您的设置"; diff --git a/apps/multiplatform/.gitignore b/apps/multiplatform/.gitignore index f30061200c..5d39eb29f2 100644 --- a/apps/multiplatform/.gitignore +++ b/apps/multiplatform/.gitignore @@ -1,5 +1,6 @@ *.iml .gradle +.kotlin /local.properties /.idea !/.idea/codeStyles/* diff --git a/apps/multiplatform/CODE.md b/apps/multiplatform/CODE.md new file mode 100644 index 0000000000..26a36e75bb --- /dev/null +++ b/apps/multiplatform/CODE.md @@ -0,0 +1,309 @@ +# 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? | +| `common/src/commonMain/` | Shared Kotlin/Compose code (Android + Desktop) | What does it **execute** on both platforms? | +| `common/src/androidMain/` | Android-specific Kotlin (platform implementations) | What does it execute on **Android**? | +| `common/src/desktopMain/` | Desktop-specific Kotlin (platform implementations) | What does it execute on **Desktop**? | +| `android/src/main/` | Android app module (Application, Activity, Services) | What is the **Android entry point**? | +| `desktop/src/jvmMain/` | Desktop app module (main function) | What is the **Desktop entry point**? | +| `../../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()`](common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#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 4–6. + +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 Kotlin data classes for value types, regular classes for reference types, and sealed classes/interfaces for variants. Why: correct type choices leverage the type system for compile-time correctness. +- Prefer exhaustive `when` expressions over `else` branches. Why: `else` branches bypass compiler checks for new sealed subclasses 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 Kotlin reader. Why: over-explaining trivial Kotlin 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. + +--- + +## Multiplatform Architecture Notes + +### Kotlin Multiplatform (KMP) + Compose Multiplatform + +The app uses Kotlin Multiplatform with Compose Multiplatform for shared UI. The project has three Gradle modules: + +- **common/** — Shared library containing all models, views, platform abstractions, and theme system +- **android/** — Android app module (Application, Activity, Services) +- **desktop/** — Desktop JVM app module (main entry point) + +### expect/actual Pattern + +Platform-specific code uses Kotlin's `expect`/`actual` mechanism. The `commonMain` source set declares `expect` functions/classes, and `androidMain`/`desktopMain` provide `actual` implementations. Files follow the naming convention: +- `commonMain`: `FileName.kt` (contains `expect` declarations) +- `androidMain`: `FileName.android.kt` (contains `actual` implementations) +- `desktopMain`: `FileName.desktop.kt` (contains `actual` implementations) + +When modifying platform abstractions, you MUST update both `actual` implementations. + +### Source Set Structure + +``` +common/src/ +├── commonMain/kotlin/chat/simplex/common/ -- Shared code (195 files) +│ ├── model/ -- ChatModel, SimpleXAPI, CryptoFile +│ ├── platform/ -- expect/actual platform abstractions +│ ├── ui/theme/ -- Theme system (ThemeManager, colors, types) +│ └── views/ -- Compose UI (chat, chatlist, call, settings, etc.) +├── androidMain/kotlin/chat/simplex/common/ -- Android actuals (55 files) +│ ├── platform/ -- actual implementations +│ └── views/ -- Android-specific view variants +├── desktopMain/kotlin/chat/simplex/common/ -- Desktop actuals (56 files) +│ ├── platform/ -- actual implementations +│ └── views/ -- Desktop-specific view variants +android/src/main/java/chat/simplex/app/ -- Android app (8 files) +desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file) +``` + +### Platform Differences + +| Aspect | Android | Desktop | +|--------|---------|---------| +| Layout | 2-column (chat list → chat) | 3-column (sidebar → chat list → details) | +| Background messaging | SimplexService (foreground service) + MessagesFetcherWorker (WorkManager) | Continuous (always-on process) | +| Notifications | Android NotificationManager with channels | Desktop system notifications | +| Calls | CallActivity (separate Activity) + CallService | In-window call view | +| Video playback | ExoPlayer | VLC (VLCJ) | +| Authentication | Android BiometricPrompt | Passcode only | +| Auto-update | Play Store / manual APK | Built-in AppUpdater | +| Window management | Standard Activity lifecycle | StoreWindowState persistence | +| Entry point | SimplexApp (Application) + MainActivity | Main.kt → initHaskell() → showApp() | + +--- + +## Document Map + +### Shared Sources (commonMain) + +| Source Location | Spec Document | Product Document | +|----------------|---------------|-----------------| +| common/.../common/App.kt | spec/architecture.md | product/views/chat-list.md | +| common/.../common/AppLock.kt | spec/architecture.md | product/views/settings.md | +| common/.../common/model/ChatModel.kt | spec/state.md | product/concepts.md | +| common/.../common/model/SimpleXAPI.kt | spec/api.md, spec/architecture.md | product/concepts.md | +| common/.../common/model/CryptoFile.kt | spec/services/files.md | product/flows/file-transfer.md | +| common/.../common/platform/Core.kt | spec/architecture.md | product/concepts.md | +| common/.../common/platform/AppCommon.kt | spec/architecture.md | product/flows/onboarding.md | +| common/.../common/platform/Notifications.kt | spec/services/notifications.md | product/flows/messaging.md | +| common/.../common/platform/NtfManager.kt | spec/services/notifications.md | product/flows/messaging.md | +| common/.../common/platform/Files.kt | spec/services/files.md | product/flows/file-transfer.md | +| common/.../common/platform/SimplexService.kt | spec/services/notifications.md | product/flows/messaging.md | +| common/.../common/platform/Share.kt | spec/architecture.md | product/concepts.md | +| common/.../common/platform/VideoPlayer.kt | spec/services/files.md | product/views/chat.md | +| common/.../common/platform/RecAndPlay.kt | spec/services/files.md | product/views/chat.md | +| common/.../common/platform/UI.kt | spec/architecture.md | product/views/chat.md | +| common/.../common/platform/Platform.kt | spec/architecture.md | product/concepts.md | +| common/.../common/ui/theme/ThemeManager.kt | spec/services/theme.md | product/views/settings.md | +| common/.../common/ui/theme/Theme.kt | spec/services/theme.md | product/views/settings.md | +| common/.../common/ui/theme/Color.kt | spec/services/theme.md | product/views/settings.md | +| common/.../common/views/chatlist/ChatListView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/ChatListNavLinkView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/ChatPreviewView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/UserPicker.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chatlist/TagListView.kt | spec/client/chat-list.md | product/views/chat-list.md | +| common/.../common/views/chat/ChatView.kt | spec/client/chat-view.md | product/views/chat.md | +| common/.../common/views/chat/ComposeView.kt | spec/client/compose.md | product/views/chat.md | +| common/.../common/views/chat/SendMsgView.kt | spec/client/compose.md | product/views/chat.md | +| common/.../common/views/chat/ChatInfoView.kt | spec/client/chat-view.md | product/views/contact-info.md | +| common/.../common/views/chat/group/ | spec/client/chat-view.md | product/views/group-info.md | +| common/.../common/views/chat/item/ | spec/client/chat-view.md | product/views/chat.md | +| common/.../common/views/call/CallView.kt | spec/services/calls.md | product/views/call.md | +| common/.../common/views/call/IncomingCallAlertView.kt | spec/services/calls.md | product/views/call.md | +| common/.../common/views/call/WebRTC.kt | spec/services/calls.md | product/flows/calling.md | +| common/.../common/views/newchat/NewChatView.kt | spec/client/navigation.md | product/views/new-chat.md | +| common/.../common/views/newchat/AddGroupView.kt | spec/client/navigation.md | product/views/new-chat.md | +| common/.../common/views/usersettings/SettingsView.kt | spec/client/navigation.md | product/views/settings.md | +| common/.../common/views/usersettings/Appearance.kt | spec/services/theme.md | product/views/settings.md | +| common/.../common/views/usersettings/PrivacySettings.kt | spec/client/navigation.md | product/views/settings.md | +| common/.../common/views/usersettings/networkAndServers/ | spec/architecture.md | product/views/settings.md | +| common/.../common/views/usersettings/UserProfilesView.kt | spec/client/navigation.md | product/views/user-profiles.md | +| common/.../common/views/onboarding/ | spec/client/navigation.md | product/views/onboarding.md | +| common/.../common/views/localauth/ | spec/architecture.md | product/views/settings.md | +| common/.../common/views/database/ | spec/database.md | product/views/settings.md | +| common/.../common/views/migration/ | spec/database.md | product/flows/onboarding.md | +| common/.../common/views/remote/ | spec/architecture.md | product/views/settings.md | +| common/.../common/views/contacts/ | spec/client/chat-view.md | product/views/contact-info.md | +| common/.../common/views/helpers/ | spec/architecture.md | product/concepts.md | + +### Android-Specific Sources + +| Source Location | Spec Document | Product Document | +|----------------|---------------|-----------------| +| android/.../app/SimplexApp.kt | spec/architecture.md | product/flows/onboarding.md | +| android/.../app/MainActivity.kt | spec/architecture.md | product/views/chat-list.md | +| android/.../app/SimplexService.kt | spec/services/notifications.md | product/flows/messaging.md | +| android/.../app/CallService.kt | spec/services/calls.md | product/flows/calling.md | +| android/.../app/MessagesFetcherWorker.kt | spec/services/notifications.md | product/flows/messaging.md | +| android/.../app/model/NtfManager.android.kt | spec/services/notifications.md | product/flows/messaging.md | +| android/.../app/views/call/CallActivity.kt | spec/services/calls.md | product/views/call.md | + +### Desktop-Specific Sources + +| Source Location | Spec Document | Product Document | +|----------------|---------------|-----------------| +| desktop/.../desktop/Main.kt | spec/architecture.md | product/flows/onboarding.md | +| common/.../common/DesktopApp.kt (desktopMain) | spec/architecture.md | product/views/chat-list.md | +| common/.../common/StoreWindowState.kt (desktopMain) | spec/architecture.md | product/views/settings.md | +| common/.../common/model/NtfManager.desktop.kt (desktopMain) | spec/services/notifications.md | product/flows/messaging.md | +| common/.../common/views/helpers/AppUpdater.kt (desktopMain) | spec/architecture.md | product/views/settings.md | + +### Haskell Core Sources (at `../../src/Simplex/Chat/` relative to `apps/multiplatform/`) + +| 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 | diff --git a/apps/multiplatform/README.md b/apps/multiplatform/README.md index e8b0e086c9..eef1048ada 100644 --- a/apps/multiplatform/README.md +++ b/apps/multiplatform/README.md @@ -1,8 +1,105 @@ # Android App Development -This readme is currently a stub and as such is in development. +This is a guide to contributing to the develop of the SimpleX android and desktop apps. -Ultimately, this readme will act as a guide to contributing to the develop of the SimpleX android app. +## Project Overview + +This is the **Kotlin Multiplatform (KMP)** mobile and desktop client for SimpleX Chat, sharing code between Android and Desktop (JVM) platforms using Compose Multiplatform for UI. + +## Build Commands + +```bash +# Android debug APK +./gradlew assembleDebug + +# Android release APK +./gradlew assembleRelease + +# Desktop distribution (current OS) +./gradlew :desktop:packageDistributionForCurrentOS + +# Run desktop/JVM tests +./gradlew desktopTest + +# Run Android instrumented tests (requires connected device/emulator) +./gradlew connectedAndroidTest + +# Build native libraries for all platforms +./gradlew common:cmakeBuild -PcrossCompile + +# Clean build +./gradlew clean +``` + +## Architecture + +### Module Structure + +- **`common/`** - Shared code (Compose UI, models, business logic) + - `src/commonMain/` - Cross-platform code + - `src/androidMain/` - Android-specific implementations + - `src/desktopMain/` - Desktop-specific implementations +- **`android/`** - Android app container +- **`desktop/`** - Desktop JVM app container + +### Key Components (`common/src/commonMain/kotlin/chat/simplex/common/`) + +- **`model/ChatModel.kt`** - Main state container with reactive properties (MutableState, MutableStateFlow) +- **`model/SimpleXAPI.kt`** - API bindings to Haskell core library via FFI +- **`platform/Core.kt`** - FFI interface to native `libapp` library +- **`platform/`** - Platform abstraction layer (expect/actual pattern for Android/Desktop specifics) +- **`views/`** - Compose UI screens organized by feature (chat, chatlist, call, usersettings, etc.) +- **`ui/theme/`** - Design system (colors, typography, shapes) + +### Native Integration + +The app calls into a Haskell core library via JNI/FFI: +- CMake builds in `common/src/commonMain/cpp/android/` and `cpp/desktop/` +- Cross-compilation toolchains in `cpp/toolchains/` +- Built libraries go to `cpp/desktop/libs/` (organized by platform) + +## Configuration + +### `local.properties` (create from `local.properties.example`) + +```properties +compression.level=0 # APK compression (0-9) +enable_debuggable=true # Debug mode +application_id.suffix=.debug # Multiple app instances on same device +app.name=SimpleX Debug # App name for debug builds +``` + +### `gradle.properties` + +Contains versions (Kotlin, Compose, AGP) and app version info. Key settings: +- `kotlin.jvm.target=11` +- `database.backend=sqlite` (or `postgres`) + +## Testing + +Tests are in: +- `common/src/commonTest/kotlin/` - Cross-platform tests +- `common/src/desktopTest/kotlin/` - Desktop-specific tests (run with `./gradlew desktopTest`) +- `android/src/androidTest/` - Android instrumented tests + +## Resources & Localization + +- String resources: `common/src/commonMain/resources/MR/base/strings.xml` + 21 language variants +- Uses Moko Resources (`dev.icerock.moko:resources`) for cross-platform resource management +- The `adjustFormatting` gradle task validates string resources during build + +## Platform-Specific Notes + +### Android +- Min SDK 26, Target SDK 35 +- NDK 23.1.7779620 +- Supports ABI splits: `arm64-v8a`, `armeabi-v7a` +- Deep linking requires SHA certificate fingerprint in `assetlinks.json` (see README.md) + +### Desktop +- Distributions: DMG (macOS), MSI/EXE (Windows), DEB (Linux) +- Mac signing/notarization configured via `local.properties` +- Video playback uses VLCJ ## Gotchas diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index 22e53af849..56279a5143 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -108,6 +108,7 @@ class ActiveCallState: Closeable { } +// Spec: spec/services/calls.md#ActiveCallView @SuppressLint("SourceLockedOrientationActivity") @Composable actual fun ActiveCallView() { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index 9d1e0c4e97..a5021ae54c 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -182,6 +182,8 @@ private fun spannableStringToAnnotatedString( actual fun getAppFileUri(fileName: String): URI = FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", if (File(fileName).isAbsolute) File(fileName) else File(getAppFilePath(fileName))).toURI() +actual fun clearImageCaches() {} + // https://developer.android.com/training/data-storage/shared/documents-files#bitmap actual suspend fun getLoadedImage(file: CIFile?): Pair? { val filePath = getLoadedFilePath(file) diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt index 059e5af426..1ebfdce6b1 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt @@ -54,11 +54,10 @@ add_library( # Sets the name of the library. simplex-api.c) add_library( simplex SHARED IMPORTED ) +FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libsimplex.${OS_LIB_EXT}) if(WIN32) - FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT}) set_target_properties( simplex PROPERTIES IMPORTED_IMPLIB ${SIMPLEXLIB}) else() - FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex-chat*.${OS_LIB_EXT}) set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB}) endif() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 70e0067260..d9439a5474 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -42,6 +42,7 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +// Spec: spec/client/navigation.md#AppScreen @Composable fun AppScreen() { AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } @@ -78,6 +79,7 @@ fun AppScreen() { } } +// Spec: spec/client/navigation.md#MainScreen @Composable fun MainScreen() { val chatModel = ChatModel @@ -289,6 +291,7 @@ fun AndroidWrapInCallLayout(content: @Composable () -> Unit) { } } +// Spec: spec/client/navigation.md#AndroidScreen @Composable fun AndroidScreen(userPickerState: MutableStateFlow) { BoxWithConstraints { @@ -402,6 +405,7 @@ fun EndPartOfScreen() { ModalManager.end.showInView() } +// Spec: spec/client/navigation.md#DesktopScreen @Composable fun DesktopScreen(userPickerState: MutableStateFlow) { Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt index d6f9640cb9..32a5ce1ef1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt @@ -13,6 +13,7 @@ import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.* +// Spec: spec/client/navigation.md#AppLock object AppLock { /** * We don't want these values to be bound to Activity lifecycle since activities are changed often, for example, when a user diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 8db2cc1a76..3d6b227df7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -81,6 +81,7 @@ val connectProgressManager = ConnectProgressManager /* * Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it * */ +// Spec: spec/state.md#ChatModel @Stable object ChatModel { val controller: ChatController = ChatController @@ -334,6 +335,7 @@ object ChatModel { } } + // Spec: spec/state.md#ChatsContext class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) { val chats = mutableStateOf(SnapshotStateList()) /** if you modify the items by adding/removing them, use helpers methods like [addToChatItems], [removeLastChatItems], [removeAllAndNotify], [clearAndNotify] and so on. @@ -1321,6 +1323,7 @@ interface SomeChat { val updatedAt: Instant } +// Spec: spec/state.md#Chat @Serializable @Stable data class Chat( val remoteHostId: Long?, @@ -1362,6 +1365,7 @@ data class Chat( true } + // Spec: spec/state.md#ChatStats @Serializable data class ChatStats( val unreadCount: Int = 0, @@ -1382,6 +1386,7 @@ data class Chat( } } +// Spec: spec/state.md#ChatInfo @Serializable sealed class ChatInfo: SomeChat, NamedChat { @@ -1899,6 +1904,12 @@ data class Connection( val connInactive: Boolean get() = quotaErrCounter >= 5 // quotaErrInactiveCount in core + val connFailedErr: String? + get() = when (connStatus) { + is ConnStatus.Failed -> connStatus.connError + else -> null + } + val connPQEnabled: Boolean get() = pqSndEnabled == true && pqRcvEnabled == true @@ -2633,25 +2644,27 @@ class PendingContactConnection( } @Serializable -enum class ConnStatus { - @SerialName("new") New, - @SerialName("prepared") Prepared, - @SerialName("joined") Joined, - @SerialName("requested") Requested, - @SerialName("accepted") Accepted, - @SerialName("snd-ready") SndReady, - @SerialName("ready") Ready, - @SerialName("deleted") Deleted; +sealed class ConnStatus { + @Serializable @SerialName("new") object New: ConnStatus() + @Serializable @SerialName("prepared") object Prepared: ConnStatus() + @Serializable @SerialName("joined") object Joined: ConnStatus() + @Serializable @SerialName("requested") object Requested: ConnStatus() + @Serializable @SerialName("accepted") object Accepted: ConnStatus() + @Serializable @SerialName("sndReady") object SndReady: ConnStatus() + @Serializable @SerialName("ready") object Ready: ConnStatus() + @Serializable @SerialName("deleted") object Deleted: ConnStatus() + @Serializable @SerialName("failed") class Failed(val connError: String): ConnStatus() val initiated: Boolean? get() = when (this) { - New -> true - Prepared -> false - Joined -> false - Requested -> true - Accepted -> true - SndReady -> null - Ready -> null - Deleted -> null + is New -> true + is Prepared -> false + is Joined -> false + is Requested -> true + is Accepted -> true + is SndReady -> null + is Ready -> null + is Deleted -> null + is Failed -> null } } @@ -4350,6 +4363,7 @@ sealed class Format { @Serializable @SerialName("strikeThrough") class StrikeThrough: Format() @Serializable @SerialName("snippet") class Snippet: Format() @Serializable @SerialName("secret") class Secret: Format() + @Serializable @SerialName("small") class Small: Format() @Serializable @SerialName("colored") class Colored(val color: FormatColor): Format() @Serializable @SerialName("uri") class Uri: Format() @Serializable @SerialName("hyperLink") class HyperLink(val showText: String?, val linkUri: String): Format() @@ -4371,6 +4385,7 @@ sealed class Format { is StrikeThrough -> SpanStyle(textDecoration = TextDecoration.LineThrough) is Snippet -> SpanStyle(fontFamily = FontFamily.Monospace) is Secret -> SpanStyle(color = Color.Transparent, background = SecretColor) + is Small -> SpanStyle(fontSize = MaterialTheme.typography.body2.fontSize, color = MaterialTheme.colors.secondary) is Colored -> SpanStyle(color = this.color.uiColor) is Uri -> linkStyle is HyperLink -> linkStyle diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt index 6ef56a9124..60f5c9e2ca 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt @@ -20,6 +20,7 @@ sealed class WriteFileResult { } * */ +// Spec: spec/services/files.md#writeCryptoFile fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs { val ctrl = ChatController.getChatCtrl() ?: throw Exception("Controller is not initialized") val buffer = ByteBuffer.allocateDirect(data.size) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index a244293edb..388a8064c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -90,6 +90,7 @@ enum class SimplexLinkMode { } } +// Spec: spec/state.md#AppPreferences class AppPreferences { // deprecated, remove in 2024 private val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true) @@ -491,6 +492,7 @@ private const val MESSAGE_TIMEOUT: Int = 300_000_000 object ChatController { private var chatCtrl: ChatCtrl? = -1 + // Spec: spec/state.md#appPrefs val appPrefs: AppPreferences by lazy { AppPreferences() } val messagesChannel: Channel = Channel() @@ -654,6 +656,7 @@ object ChatController { chatModel.updateChatTags(rhId) } + // Spec: spec/api.md#startReceiver private fun startReceiver() { Log.d(TAG, "ChatController startReceiver") if (receiverJob != null || chatCtrl == null) return @@ -797,6 +800,7 @@ object ChatController { return null } + // Spec: spec/api.md#sendCmd suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, retryNum: Int = 0, log: Boolean = true): API { val ctrl = otherCtrl ?: chatCtrl ?: throw Exception("Controller is not initialized") @@ -821,6 +825,7 @@ object ChatController { } } + // Spec: spec/api.md#recvMsg fun recvMsg(ctrl: ChatCtrl): API? { val rStr = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) return if (rStr == "") { @@ -1036,6 +1041,14 @@ object ChatController { return null } + suspend fun apiGetChatContentTypes(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): List? { + val r = sendCmd(rh, CC.ApiGetChatContentTypes(type, id, scope)) + if (r is API.Result && r.res is CR.ChatContentTypes) return r.res.contentTypes + Log.e(TAG, "apiGetChatContentTypes bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_loading_details), "${r.responseType}: ${r.details}") + return null + } + suspend fun apiCreateChatTag(rh: Long?, tag: ChatTagData): List? { val r = sendCmd(rh, CC.ApiCreateChatTag(tag)) if (r is API.Result && r.res is CR.ChatTags) return r.res.userTags @@ -2551,6 +2564,7 @@ object ChatController { AlertManager.shared.showAlertMsg(title, errMsg) } + // Spec: spec/api.md#processReceivedMsg private suspend fun processReceivedMsg(msg: API) { lastMsgReceivedTimestamp = System.currentTimeMillis() val rhId = msg.rhId @@ -3511,6 +3525,7 @@ class SharedPreference(val get: () -> T, set: (T) -> Unit) { } // ChatCommand +// Spec: spec/api.md#CC sealed class CC { class Console(val cmd: String): CC() class ShowActiveUser: CC() @@ -3542,6 +3557,7 @@ sealed class CC { class ApiGetChatTags(val userId: Long): CC() class ApiGetChats(val userId: Long): CC() class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC() + class ApiGetChatContentTypes(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC() class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC() class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val live: Boolean, val ttl: Int?, val composedMessages: List): CC() class ApiCreateChatTag(val tag: ChatTagData): CC() @@ -3726,6 +3742,7 @@ sealed class CC { } "/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") } + is ApiGetChatContentTypes -> "/_get content types ${chatRef(type, id, scope)}" is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId" is ApiSendMessages -> { val msgs = json.encodeToString(composedMessages) @@ -3912,6 +3929,7 @@ sealed class CC { is ApiGetChatTags -> "apiGetChatTags" is ApiGetChats -> "apiGetChats" is ApiGetChat -> "apiGetChat" + is ApiGetChatContentTypes -> "apiGetChatContentTypes" is ApiGetChatItemInfo -> "apiGetChatItemInfo" is ApiSendMessages -> "apiSendMessages" is ApiCreateChatTag -> "apiCreateChatTag" @@ -4139,9 +4157,11 @@ class UpdatedMessage(val msgContent: MsgContent, val mentions: Map @Serializable class ChatTagData(val emoji: String?, val text: String) +// Spec: spec/api.md#ArchiveConfig @Serializable class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null) +// Spec: spec/database.md#DBEncryptionConfig @Serializable class DBEncryptionConfig(val currentKey: String, val newKey: String) @@ -5949,6 +5969,7 @@ val yaml = Yaml(configuration = YamlConfiguration( codePointLimit = 5500000, )) +// Spec: spec/api.md#API @Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Serializable(with = APISerializer::class) sealed class API { @@ -6088,6 +6109,7 @@ private fun decodeObject(deserializer: DeserializationStrategy, obj: Json runCatching { json.decodeFromJsonElement(deserializer, obj!!) }.getOrNull() // ChatResponse +// Spec: spec/api.md#CR @Serializable sealed class CR { @Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR() @@ -6097,6 +6119,7 @@ sealed class CR { @Serializable @SerialName("chatStopped") class ChatStopped: CR() @Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List): CR() @Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR() + @Serializable @SerialName("chatContentTypes") class ChatContentTypes(val contentTypes: List): CR() @Serializable @SerialName("chatTags") class ChatTags(val user: UserRef, val userTags: List): CR() @Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR() @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() @@ -6278,6 +6301,7 @@ sealed class CR { is ChatStopped -> "chatStopped" is ApiChats -> "apiChats" is ApiChat -> "apiChat" + is ChatContentTypes -> "chatContentTypes" is ChatTags -> "chatTags" is ApiChatItemInfo -> "chatItemInfo" is ServerTestResult -> "serverTestResult" @@ -6451,6 +6475,7 @@ sealed class CR { is ChatStopped -> noDetails() is ApiChats -> withUser(user, json.encodeToString(chats)) is ApiChat -> withUser(user, "remoteHostId: ${chat.remoteHostId}\nchatInfo: ${chat.chatInfo}\nchatStats: ${chat.chatStats}\nnavInfo: ${navInfo}\nchatItems: ${chat.chatItems}") + is ChatContentTypes -> "content types: ${json.encodeToString(contentTypes)}" is ChatTags -> withUser(user, "userTags: ${json.encodeToString(userTags)}") is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}") is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}") @@ -6944,6 +6969,7 @@ data class RemoteFile( val fileSource: CryptoFile ) +// Spec: spec/api.md#ChatError @Serializable sealed class ChatError { val string: String get() = when (this) { @@ -7627,6 +7653,7 @@ sealed class RCErrorType { @Serializable @SerialName("syntax") data class SYNTAX(val syntaxErr: String): RCErrorType() } +// Spec: spec/database.md#ArchiveError @Serializable sealed class ArchiveError { val string: String get() = when (this) { @@ -7708,6 +7735,7 @@ sealed class RemoteCtrlError { @Serializable @SerialName("protocolError") object ProtocolError: RemoteCtrlError() } +// Spec: spec/services/notifications.md#NotificationsMode enum class NotificationsMode() { OFF, PERIODIC, SERVICE, /*INSTANT - for Firebase notifications */; diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index d0ce703033..36a7ae1a80 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -14,12 +14,14 @@ import java.io.File import java.nio.ByteBuffer // ghc's rts +// Spec: spec/architecture.md#initHS external fun initHS() // android-support external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long +// Spec: spec/architecture.md#chatMigrateInit external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array external fun chatCloseStore(ctrl: ChatCtrl): String external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String @@ -45,6 +47,7 @@ val appPreferences: AppPreferences val chatController: ChatController = ChatController +// Spec: spec/architecture.md#initChatControllerOnStart fun initChatControllerOnStart() { withLongRunningApi { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { @@ -55,6 +58,7 @@ fun initChatControllerOnStart() { } } +// Spec: spec/architecture.md#initChatController suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred = { CompletableDeferred(true) }) { Log.d(TAG, "initChatController") try { @@ -182,6 +186,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } } +// Spec: spec/architecture.md#chatInitTemporaryDatabase fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation: MigrationConfirmation = MigrationConfirmation.Error): Pair { val dbKey = key ?: randomDatabasePassword() Log.d(TAG, "chatInitTemporaryDatabase path: $dbPath") @@ -193,6 +198,7 @@ fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation: return res to migrated[1] as ChatCtrl } +// Spec: spec/architecture.md#chatInitControllerRemovingDatabases fun chatInitControllerRemovingDatabases() { val dbPath = dbAbsolutePrefixPath // Remove previous databases, otherwise, can be .errorNotADatabase with null controller diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index 0a4f670fe0..88d9fbb705 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -14,6 +14,7 @@ import java.net.URLEncoder import java.nio.file.Files import java.nio.file.StandardCopyOption +// Spec: spec/services/files.md#dataDir expect val dataDir: File expect val tmpDir: File expect val filesDir: File diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index d906ef7baf..39fcea3981 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -13,6 +13,7 @@ enum class NotificationAction { ACCEPT_CONTACT_REQUEST } +// Spec: spec/services/notifications.md#ntfManager lateinit var ntfManager: NtfManager abstract class NtfManager { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index df9af7fbf6..1b5a81a819 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -22,6 +22,7 @@ import chat.simplex.res.MR import kotlinx.serialization.Transient import java.util.UUID +// Spec: spec/services/theme.md#DefaultTheme enum class DefaultTheme { LIGHT, DARK, SIMPLEX, BLACK; @@ -47,6 +48,7 @@ enum class DefaultThemeMode { @SerialName("dark") DARK } +// Spec: spec/services/theme.md#AppColors @Stable class AppColors( title: Color, @@ -99,6 +101,7 @@ class AppColors( } } +// Spec: spec/services/theme.md#AppWallpaper @Stable class AppWallpaper( background: Color? = null, @@ -133,6 +136,7 @@ class AppWallpaper( } } +// Spec: spec/services/theme.md#ThemeColor enum class ThemeColor { PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, SENT_QUOTE, RECEIVED_MESSAGE, RECEIVED_QUOTE, PRIMARY_VARIANT2, WALLPAPER_BACKGROUND, WALLPAPER_TINT; @@ -174,6 +178,7 @@ enum class ThemeColor { } } +// Spec: spec/services/theme.md#ThemeColors @Serializable data class ThemeColors( @SerialName("accent") @@ -214,6 +219,7 @@ data class ThemeColors( } } +// Spec: spec/services/theme.md#ThemeWallpaper @Serializable data class ThemeWallpaper ( val preset: String? = null, @@ -293,6 +299,7 @@ data class ThemesFile( val themes: List = emptyList() ) +// Spec: spec/services/theme.md#ThemeOverrides @Serializable data class ThemeOverrides ( val themeId: String = UUID.randomUUID().toString(), @@ -463,6 +470,7 @@ fun List.skipDuplicates(): List { return res } +// Spec: spec/services/theme.md#ThemeModeOverrides @Serializable data class ThemeModeOverrides ( val light: ThemeModeOverride? = null, @@ -474,6 +482,7 @@ data class ThemeModeOverrides ( } } +// Spec: spec/services/theme.md#ThemeModeOverride @Serializable data class ThemeModeOverride ( val mode: DefaultThemeMode = CurrentColors.value.base.mode, @@ -714,6 +723,7 @@ val BlackColorPaletteApp = AppColors( var systemInDarkThemeCurrently: Boolean = isInNightMode() +// Spec: spec/services/theme.md#CurrentColors val CurrentColors: MutableStateFlow = MutableStateFlow(ThemeManager.currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())) @Composable @@ -758,6 +768,7 @@ fun reactOnDarkThemeChanges(isDark: Boolean) { } } +// Spec: spec/services/theme.md#SimpleXTheme @Composable fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { // TODO: Fix preview working with dark/light theme diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 07f2b678cf..7d8c79b4a8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -53,6 +53,7 @@ object ThemeManager { ?: ThemeWallpaper.from(PresetWallpaper.SCHOOL.toType(CurrentColors.value.base), null, null)) } + // Spec: spec/services/theme.md#currentColors fun currentColors(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: List): ActiveTheme { val themeName = appPrefs.currentTheme.get()!! val nonSystemThemeName = nonSystemThemeName() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt index 8f5aba138d..7a92bc8c39 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt @@ -6,6 +6,7 @@ import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import kotlinx.coroutines.* +// Spec: spec/services/calls.md#ActiveCallView @Composable expect fun ActiveCallView() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt index 4d8c1fae46..563f4c3b83 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt @@ -22,6 +22,7 @@ import chat.simplex.common.views.usersettings.ProfilePreview import chat.simplex.res.MR import kotlinx.datetime.Clock +// Spec: spec/services/calls.md#IncomingCallAlertView @Composable fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) { val cm = chatModel.callManager diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt index 705fc6a28f..6fa99283d8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt @@ -46,6 +46,7 @@ data class Call( get() = localMediaSources.hasVideo || peerMediaSources.hasVideo } +// Spec: spec/services/calls.md#CallState enum class CallState { WaitCapabilities, InvitationSent, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt index ed40150cb1..107d427556 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -27,11 +27,12 @@ suspend fun apiLoadMessages( chatType: ChatType, apiId: Long, pagination: ChatPagination, + contentTag: MsgContentTag? = null, search: String = "", openAroundItemId: Long? = null, visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } ) = coroutineScope { - val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, pagination, search) ?: return@coroutineScope + val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), contentTag ?: chatsCtx.contentTag, pagination, search) ?: return@coroutineScope // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes /** When [openAroundItemId] is provided, chatId can be different too */ if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 26daee363f..c518b156d9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -45,6 +45,7 @@ import chat.simplex.common.views.newchat.ContactConnectionInfoView import chat.simplex.common.views.newchat.alertProfileImageSize import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.datetime.* @@ -92,6 +93,7 @@ fun ConnectInProgressView(s: String) { @Composable // staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat // to chat list smooth. Otherwise, chat view will become blank right before the transition starts +// Spec: spec/client/chat-view.md#ChatView fun ChatView( chatsCtx: ChatModel.ChatsContext, staleChatId: State, @@ -144,6 +146,9 @@ fun ChatView( val scope = rememberCoroutineScope() val selectedChatItems = rememberSaveable { mutableStateOf(null as Set?) } val showCommandsMenu = rememberSaveable { mutableStateOf(false) } + val contentFilter = rememberSaveable { mutableStateOf(null) } + val availableContent = remember { mutableStateOf>(ContentFilter.initialList) } + if (appPlatform.isAndroid) { DisposableEffect(Unit) { onDispose { @@ -170,7 +175,13 @@ fun ChatView( } showSearch.value = false searchText.value = "" + contentFilter.value = null + availableContent.value = ContentFilter.initialList selectedChatItems.value = null + val cInfo = activeChat.value?.chatInfo + if (chatsCtx.secondaryContextFilter == null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group || cInfo is ChatInfo.Local)) { + updateAvailableContent(chatRh, activeChat, availableContent) + } if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) { withBGApi { val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) @@ -229,11 +240,11 @@ fun ChatView( val sameText = searchText.value == value // showSearch can be false with empty text when it was closed manually after clicking on message from search to load .around it // (required on Android to have this check to prevent call to search with old text) - val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.secondaryContextFilter == null + val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.secondaryContextFilter == null && contentFilter.value == null val c = chatModel.getChat(chatInfo.id) - if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged + if ((sameText && contentFilter.value == null) || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged withBGApi { - apiFindMessages(chatsCtx, c, value) + apiFindMessages(chatsCtx, c, contentFilter.value?.contentTag, value) searchText.value = value } } @@ -486,7 +497,7 @@ fun ChatView( val c = chatModel.getChat(chatId) if (chatModel.chatId.value != chatId) return@ChatLayout if (c != null) { - apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, searchText.value, null, visibleItemIndexes) + apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, contentFilter.value?.contentTag, searchText.value, null, visibleItemIndexes) } }, deleteMessage = { itemId, mode -> @@ -742,14 +753,24 @@ fun ChatView( changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) }, onSearchValueChanged = onSearchValueChanged, closeSearch = { + onSearchValueChanged("") showSearch.value = false searchText.value = "" + contentFilter.value = null + // Update available content types when search closes + val cInfo = activeChat.value?.chatInfo + if (chatsCtx.secondaryContextFilter == null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group || cInfo is ChatInfo.Local)) { + updateAvailableContent(chatRh, activeChat, availableContent) + } }, onComposed, developerTools = chatModel.controller.appPrefs.developerTools.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), showSearch = showSearch, - showCommandsMenu = showCommandsMenu + showCommandsMenu = showCommandsMenu, + contentFilter = contentFilter, + availableContent = availableContent, + searchPlaceholder = contentFilter.value?.searchPlaceholder?.let { generalGetString(it) } ) } } @@ -785,6 +806,23 @@ fun ChatView( } } +fun updateAvailableContent(chatRh: Long?, activeChat: State, availableContent: MutableState>) { + withBGApi { + Log.e(TAG, "updateAvailableContent") + val chatInfo = activeChat.value?.chatInfo + if (chatInfo == null) return@withBGApi + val types = chatModel.controller.apiGetChatContentTypes(chatRh, chatInfo.chatType, chatInfo.apiId, null) + if (activeChat.value?.chatInfo?.id != chatInfo.id) return@withBGApi + if (types == null) { + availableContent.value = ContentFilter.entries + } else { + val typeSet: Set = types.union(ContentFilter.alwaysShow) + Log.e(TAG, "updateAvailableContent $typeSet") + availableContent.value = ContentFilter.entries.filter { it -> typeSet.contains(it.contentTag) } + } + } +} + private fun connectingText(chatInfo: ChatInfo): String? { return when (chatInfo) { is ChatInfo.Direct -> @@ -879,7 +917,10 @@ fun ChatLayout( developerTools: Boolean, showViaProxy: Boolean, showSearch: MutableState, - showCommandsMenu: MutableState + showCommandsMenu: MutableState, + contentFilter: MutableState, + availableContent: State>, + searchPlaceholder: String? ) { val chatInfo = remember { derivedStateOf { chat.value?.chatInfo } } val scope = rememberCoroutineScope() @@ -1063,7 +1104,7 @@ fun ChatLayout( Box { if (selectedChatItems.value == null) { if (chatInfo != null) { - ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch, contentFilter, availableContent, searchPlaceholder) } } else { SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value) @@ -1096,10 +1137,14 @@ fun BoxScope.ChatInfoToolbar( openGroupLink: (GroupInfo) -> Unit, changeNtfsState: (MsgFilter, currentValue: MutableState) -> Unit, onSearchValueChanged: (String) -> Unit, - showSearch: MutableState + showSearch: MutableState, + contentFilter: MutableState, + availableContent: State>, + searchPlaceholder: String? ) { val scope = rememberCoroutineScope() val showMenu = rememberSaveable { mutableStateOf(false) } + val showContentFilterMenu = rememberSaveable { mutableStateOf(false) } val onBackClicked = { if (!showSearch.value) { @@ -1107,6 +1152,7 @@ fun BoxScope.ChatInfoToolbar( } else { onSearchValueChanged("") showSearch.value = false + contentFilter.value = null } } if (appPlatform.isAndroid && chatsCtx.secondaryContextFilter == null) { @@ -1115,104 +1161,141 @@ fun BoxScope.ChatInfoToolbar( val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>() val activeCall by remember { chatModel.activeCall } - if (chatInfo is ChatInfo.Local) { - barButtons.add { - IconButton( - { - showMenu.value = false - showSearch.value = true - }, enabled = chatInfo.noteFolder.ready - ) { - Icon( - painterResource(MR.images.ic_search), - stringResource(MR.strings.search_verb).capitalize(Locale.current), - tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - ) - } - } - } else { - menuItems.add { - ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { - showMenu.value = false - showSearch.value = true - }) - } - } - if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) { - if (activeCall == null) { - barButtons.add { - IconButton({ - showMenu.value = false - startCall(CallMediaType.Audio) - }, enabled = chatInfo.contact.ready && chatInfo.contact.active - ) { - Icon( - painterResource(MR.images.ic_call_500), - stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), - tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - ) - } - } - } else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) { - barButtons.add { - val call = remember { chatModel.activeCall }.value - val connectedAt = call?.connectedAt - if (connectedAt != null) { - val time = remember { mutableStateOf(durationText(0)) } - LaunchedEffect(Unit) { - while (true) { - time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt()) - delay(250) - } - } - val sp50 = with(LocalDensity.current) { 50.sp.toDp() } - Text(time.value, Modifier.widthIn(min = sp50)) - } - } - barButtons.add { - IconButton({ - showMenu.value = false - endCall() - }) { - Icon( - painterResource(MR.images.ic_call_end_filled), - null, - tint = MaterialTheme.colors.error - ) - } - } - } - if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) { + val showContentFilterButton = availableContent.value.isNotEmpty() + val activeCallInChat = chatInfo is ChatInfo.Direct && activeCall?.contact?.id == chatInfo.id + + // Content filter button - shown in bar, or moved to menu during active call + if (showContentFilterButton) { + val enabled = chatInfo !is ChatInfo.Local || chatInfo.noteFolder.ready + if (activeCallInChat) { menuItems.add { - ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { - showMenu.value = false - startCall(CallMediaType.Video) - }) - } - } - } else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) { - if (!chatInfo.incognito) { - barButtons.add { - IconButton({ - showMenu.value = false - addMembers(chatInfo.groupInfo) - }) { - Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary) - } + ItemAction( + stringResource(MR.strings.content_filter_menu_item), + painterResource(MR.images.ic_photo_library), + onClick = { + showMenu.value = false + showContentFilterMenu.value = true + } + ) } } else { barButtons.add { - IconButton({ - showMenu.value = false - openGroupLink(chatInfo.groupInfo) - }) { - Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary) + IconButton( + { showContentFilterMenu.value = true }, + enabled = enabled + ) { + Icon( + painterResource(MR.images.ic_photo_library), + null, + tint = MaterialTheme.colors.primary + ) } } } } + // Chat-type specific buttons + when (chatInfo) { + is ChatInfo.Local -> { + barButtons.add { + IconButton( + { + showMenu.value = false + showSearch.value = true + }, enabled = chatInfo.noteFolder.ready + ) { + Icon( + painterResource(MR.images.ic_search), + stringResource(MR.strings.search_verb).capitalize(Locale.current), + tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + } + } + } + is ChatInfo.Direct -> { + if (activeCall?.contact?.id == chatInfo.id) { + if (appPlatform.isDesktop) { + barButtons.add { + val call = remember { chatModel.activeCall }.value + val connectedAt = call?.connectedAt + if (connectedAt != null) { + val time = remember { mutableStateOf(durationText(0)) } + LaunchedEffect(Unit) { + while (true) { + time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt()) + delay(250) + } + } + val sp50 = with(LocalDensity.current) { 50.sp.toDp() } + Text(time.value, Modifier.widthIn(min = sp50)) + } + } + } + barButtons.add { + IconButton({ + showMenu.value = false + endCall() + }) { + Icon( + painterResource(MR.images.ic_call_end_filled), + null, + tint = MaterialTheme.colors.error + ) + } + } + } + // Call buttons moved to menu + if (chatInfo.contact.mergedPreferences.calls.enabled.forUser && chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) { + menuItems.add { + ItemAction(stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), painterResource(MR.images.ic_call_500), onClick = { + showMenu.value = false + startCall(CallMediaType.Audio) + }) + } + menuItems.add { + ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { + showMenu.value = false + startCall(CallMediaType.Video) + }) + } + } + menuItems.add { + ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { + showMenu.value = false + showSearch.value = true + }) + } + } + is ChatInfo.Group -> { + // Add members / group link moved to menu + if (chatInfo.groupInfo.canAddMembers) { + if (!chatInfo.incognito) { + menuItems.add { + ItemAction(stringResource(MR.strings.icon_descr_add_members), painterResource(MR.images.ic_person_add_500), onClick = { + showMenu.value = false + addMembers(chatInfo.groupInfo) + }) + } + } else { + menuItems.add { + ItemAction(stringResource(MR.strings.group_link), painterResource(MR.images.ic_add_link), onClick = { + showMenu.value = false + openGroupLink(chatInfo.groupInfo) + }) + } + } + } + menuItems.add { + ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { + showMenu.value = false + showSearch.value = true + }) + } + } + else -> {} + } + val enableNtfs = chatInfo.chatSettings?.enableNtfs if (((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) && enableNtfs != null) { val ntfMode = remember { mutableStateOf(enableNtfs) } @@ -1242,13 +1325,27 @@ fun BoxScope.ChatInfoToolbar( } val oneHandUI = remember { appPrefs.oneHandUI.state } val chatBottomBar = remember { appPrefs.chatBottomBar.state } + val searchTrailingContent: @Composable (() -> Unit)? = if (showContentFilterButton) {{ + IconButton({ showContentFilterMenu.value = true }) { + Icon( + painterResource(if (contentFilter.value == null) MR.images.ic_photo_library else MR.images.ic_photo_library_filled), + null, + Modifier.padding(4.dp), + tint = MaterialTheme.colors.primary + ) + } + }} else null + DefaultAppBar( navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, title = { ChatInfoToolbarTitle(chatInfo) }, onTitleClick = if (chatInfo is ChatInfo.Local) null else info, showSearch = showSearch.value, + searchAlwaysVisible = contentFilter.value != null, onTop = !oneHandUI.value || !chatBottomBar.value, + searchPlaceholder = searchPlaceholder, onSearchValueChanged = onSearchValueChanged, + searchTrailingContent = searchTrailingContent, buttons = { barButtons.forEach { it() } } ) Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) { @@ -1269,6 +1366,65 @@ fun BoxScope.ChatInfoToolbar( menuItems.forEach { it() } } } + val contentFilterWidth = remember { mutableStateOf(250.dp) } + val contentFilterHeight = remember { mutableStateOf(0.dp) } + DefaultDropdownMenu( + showContentFilterMenu, + modifier = Modifier.onSizeChanged { with(density) { + contentFilterWidth.value = it.width.toDp().coerceAtLeast(250.dp) + if (oneHandUI.value && chatBottomBar.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) contentFilterHeight.value = it.height.toDp() + } }, + offset = DpOffset(-contentFilterWidth.value, if (oneHandUI.value && chatBottomBar.value) -contentFilterHeight.value else AppBarHeight) + ) { + val contentFilterMenuItems: List<@Composable () -> Unit> = buildList { + availableContent.value.forEach { filter -> + val isSelected = contentFilter.value == filter + add { + ItemAction( + stringResource(filter.label), + painterResource(if (isSelected) filter.iconFilled else filter.icon), + color = if (isSelected) MaterialTheme.colors.primary else Color.Unspecified, + onClick = { + showContentFilterMenu.value = false + if (contentFilter.value == filter) return@ItemAction + contentFilter.value = filter + showSearch.value = true + scope.launch { + val c = chatModel.getChat(chatInfo.id) + if (c != null) { + apiFindMessages(chatsCtx, c, filter.contentTag, "") + } + } + } + ) + } + } + if (showSearch.value) { + add { + ItemAction( + stringResource(MR.strings.content_filter_all_messages), + painterResource(MR.images.ic_forum), + onClick = { + showContentFilterMenu.value = false + contentFilter.value = null + showSearch.value = false + scope.launch { + val c = chatModel.getChat(chatInfo.id) + if (c != null) { + apiFindMessages(chatsCtx, c, null, "") + } + } + } + ) + } + } + } + if (oneHandUI.value && chatBottomBar.value) { + contentFilterMenuItems.asReversed().forEach { it() } + } else { + contentFilterMenuItems.forEach { it() } + } + } } } @@ -3425,7 +3581,10 @@ fun PreviewChatLayout() { developerTools = false, showViaProxy = false, showSearch = remember { mutableStateOf(false) }, - showCommandsMenu = remember { mutableStateOf(false) } + showCommandsMenu = remember { mutableStateOf(false) }, + contentFilter = remember { mutableStateOf(null) }, + availableContent = remember { mutableStateOf(ContentFilter.initialList) }, + searchPlaceholder = null ) } } @@ -3505,7 +3664,30 @@ fun PreviewGroupChatLayout() { developerTools = false, showViaProxy = false, showSearch = remember { mutableStateOf(false) }, - showCommandsMenu = remember { mutableStateOf(false) } + showCommandsMenu = remember { mutableStateOf(false) }, + contentFilter = remember { mutableStateOf(null) }, + availableContent = remember { mutableStateOf(ContentFilter.initialList) }, + searchPlaceholder = null ) } } + +enum class ContentFilter( + val contentTag: MsgContentTag, + val label: StringResource, + val searchPlaceholder: StringResource, + val icon: ImageResource, + val iconFilled: ImageResource +) { + Images(MsgContentTag.Image, MR.strings.content_filter_images, MR.strings.placeholder_search_images, MR.images.ic_image, MR.images.ic_image_filled), + Videos(MsgContentTag.Video, MR.strings.content_filter_videos, MR.strings.placeholder_search_videos, MR.images.ic_videocam, MR.images.ic_videocam_filled), + Voice(MsgContentTag.Voice, MR.strings.content_filter_voice_messages, MR.strings.placeholder_search_voice_messages, MR.images.ic_mic, MR.images.ic_mic_filled), + Files(MsgContentTag.File, MR.strings.content_filter_files, MR.strings.placeholder_search_files, MR.images.ic_draft, MR.images.ic_draft_filled), + Links(MsgContentTag.Link, MR.strings.content_filter_links, MR.strings.placeholder_search_links, MR.images.ic_link, MR.images.ic_link); + + companion object { + val alwaysShow: Set = setOf(MsgContentTag.Image, MsgContentTag.Link) + + val initialList: List = listOf(ContentFilter.Images, ContentFilter.Files, ContentFilter.Links) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index af4baad90f..eebf4a7bf8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -47,6 +47,7 @@ import kotlin.math.min const val MAX_NUMBER_OF_MENTIONS = 3 +// Spec: spec/client/compose.md#ComposePreview @Serializable sealed class ComposePreview { @Serializable object NoPreview: ComposePreview() @@ -92,6 +93,7 @@ object ComposeMessageSerializer : KSerializer { decoder.decodeLong().let { value -> TextRange(unpackInt1(value), unpackInt2(value)) } } +// Spec: spec/client/compose.md#ComposeState @Serializable data class ComposeState( val message: ComposeMessage = ComposeMessage(), @@ -259,6 +261,7 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview { } } +// Spec: spec/client/compose.md#AttachmentSelection @Composable expect fun AttachmentSelection( composeState: MutableState, @@ -341,6 +344,7 @@ suspend fun MutableState.processPickedMedia(uris: List, text: } } +// Spec: spec/client/compose.md#ComposeView @Composable fun ComposeView( rhId: Long?, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 467f1e52af..4de0175457 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -31,6 +31,7 @@ import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.* import java.net.URI +// Spec: spec/client/compose.md#SendMsgView @Composable fun SendMsgView( composeState: MutableState, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 3f80361249..dd3374d50b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -878,9 +878,11 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr } fun memberConnStatus(): String { - return if (member.activeConn?.connDisabled == true) { - generalGetString(MR.strings.member_info_member_disabled) + return if (member.activeConn?.connStatus is ConnStatus.Failed) { + generalGetString(MR.strings.member_info_member_failed) } else if (member.activeConn?.connDisabled == true) { + generalGetString(MR.strings.member_info_member_disabled) + } else if (member.activeConn?.connInactive == true) { generalGetString(MR.strings.member_info_member_inactive) } else { member.memberStatus.shortText diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index f09d2f44bb..8902a0fd9e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -562,6 +562,14 @@ fun GroupMemberInfoLayout( } } + val connFailedErr = member.activeConn?.connFailedErr + if (connFailedErr != null) { + SectionDividerSpaced() + SectionView { + InfoRow(stringResource(MR.strings.info_row_connection_failed), connFailedErr) + } + } + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { ModeratorDestructiveSection() } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt index e696128288..c3cf954ab6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt @@ -162,7 +162,9 @@ private fun ModalData.MemberSupportViewLayout( @Composable fun SupportChatRow(member: GroupMember) { fun memberStatus(): String { - return if (member.activeConn?.connDisabled == true) { + return if (member.activeConn?.connStatus is ConnStatus.Failed) { + generalGetString(MR.strings.member_info_member_failed) + } else if (member.activeConn?.connDisabled == true) { generalGetString(MR.strings.member_info_member_disabled) } else if (member.activeConn?.connInactive == true) { generalGetString(MR.strings.member_info_member_inactive) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 1be2110b1f..064b5370bc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -26,7 +26,7 @@ import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH import chat.simplex.common.views.chat.chatViewScrollState import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* @Composable fun CIImageView( @@ -38,6 +38,7 @@ fun CIImageView( receiveFile: (Long) -> Unit ) { val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } + val previewBitmap = remember(image) { base64ToBitmap(image) } @Composable fun progressIndicator() { CircularProgressIndicator( @@ -144,7 +145,7 @@ fun CIImageView( .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), contentAlignment = Alignment.Center ) { - imageView(base64ToBitmap(image), onClick = { + imageView(previewBitmap, onClick = { if (fileSource != null) { openFile(fileSource) } @@ -178,14 +179,16 @@ fun CIImageView( Box( Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID) + .then( + if (!smallView) { + val w = if (previewBitmap.width * 0.97 <= previewBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH + Modifier.width(w).aspectRatio(previewBitmap.width.toFloat() / previewBitmap.height.toFloat()) + } else Modifier + ) .desktopModifyBlurredState(!smallView, blurred, showMenu), contentAlignment = Alignment.TopEnd ) { - val res: MutableState?> = remember { - mutableStateOf( - if (chatModel.connectedToRemote()) null else runBlocking { imageAndFilePath(file) } - ) - } + val res: MutableState?> = remember { mutableStateOf(null) } if (chatModel.connectedToRemote()) { LaunchedEffect(file, CIFile.cachedRemoteFileRequests.toList()) { withBGApi { @@ -195,9 +198,9 @@ fun CIImageView( } } } else { - KeyChangeEffect(file) { + LaunchedEffect(file) { if (res.value == null || res.value!!.third != getLoadedFilePath(file)) { - res.value = imageAndFilePath(file) + res.value = withContext(Dispatchers.IO) { imageAndFilePath(file) } } } } @@ -206,7 +209,7 @@ fun CIImageView( val (imageBitmap, data, _) = loaded SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) }) } else { - imageView(base64ToBitmap(image), onClick = { + imageView(previewBitmap, onClick = { if (file != null) { when { file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 758980059d..633d6c454e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -61,6 +61,7 @@ data class ChatItemReactionMenuItem ( val onClick: (() -> Unit)? ) +// Spec: spec/client/chat-view.md#ChatItemView @Composable fun ChatItemView( chatsCtx: ChatModel.ChatsContext, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index f36da6c908..900fa238a5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -144,7 +144,7 @@ fun FramedItemView( Box(Modifier.fillMaxWidth().weight(1f)) { ciQuotedMsgView(qi) } - val imageBitmap = base64ToBitmap(qi.content.image) + val imageBitmap = remember(qi.content.image) { base64ToBitmap(qi.content.image) } Image( imageBitmap, contentDescription = stringResource(MR.strings.image_descr), @@ -156,7 +156,7 @@ fun FramedItemView( Box(Modifier.fillMaxWidth().weight(1f)) { ciQuotedMsgView(qi) } - val imageBitmap = base64ToBitmap(qi.content.image) + val imageBitmap = remember(qi.content.image) { base64ToBitmap(qi.content.image) } Image( imageBitmap, contentDescription = stringResource(MR.strings.video_descr), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index 70d6fa4aa8..8d96102daa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -158,7 +158,10 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> val uriDecrypted = remember(media.uri.path) { mutableStateOf(if (media.fileSource?.cryptoArgs == null) media.uri else media.fileSource.decryptedGet()) } val decrypted = uriDecrypted.value if (decrypted != null) { - VideoView(modifier, decrypted, preview, index == settledCurrentPage, close) + // settledCurrentPage finishes **only** when fully swiped + // So we use pagerState.currentPage that changes right away as the screen is being dragged + val isCurrentPage = index == pagerState.currentPage && kotlin.math.abs(pagerState.currentPageOffsetFraction) < 0.3f + VideoView(modifier, decrypted, preview, isCurrentPage, close) DisposableEffect(Unit) { onDispose { playersToRelease.add(decrypted) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 60595fc255..3984e5bc40 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -153,6 +153,7 @@ fun MarkdownText ( is Format.Italic -> withStyle(ft.format.style) { append(ft.text) } is Format.StrikeThrough -> withStyle(ft.format.style) { append(ft.text) } is Format.Snippet -> withStyle(ft.format.style) { append(ft.text) } + is Format.Small -> withStyle(ft.format.style) { append(ft.text) } is Format.Colored -> withStyle(ft.format.style) { append(ft.text) } is Format.Secret -> { val ftStyle = ft.format.style diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 77ab62dbf1..293a93b15a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -32,6 +32,7 @@ import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.datetime.Clock +// Spec: spec/client/chat-list.md#ChatListNavLinkView @Composable fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { val showMenu = remember { mutableStateOf(false) } @@ -228,6 +229,7 @@ suspend fun openChat( } else { ChatPagination.Initial(ChatPagination.INITIAL_COUNT) }, + contentTag = null, "", openAroundItemId ) @@ -241,11 +243,12 @@ suspend fun openLoadedChat(chat: Chat) { } } -suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, search: String) { +suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, contentTag: MsgContentTag?, search: String) { withContext(Dispatchers.Main) { chatsCtx.chatItems.clearAndNotify() } - apiLoadMessages(chatsCtx, ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = if (search.isNotEmpty()) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT), search = search) + val pagination = if (search.isNotEmpty() || contentTag != null) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT) + apiLoadMessages(chatsCtx, ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination, contentTag, search) } suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 2109e21bfe..a42f66c6cf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -122,6 +122,7 @@ fun ToggleChatListCard() { } } +// Spec: spec/client/chat-list.md#ChatListView @Composable fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { val oneHandUI = remember { appPrefs.oneHandUI.state } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 4280845867..9248ac6efe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -35,6 +35,7 @@ import chat.simplex.common.views.chat.item.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +// Spec: spec/client/chat-list.md#ChatPreviewView @Composable fun ChatPreviewView( chat: Chat, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt index 8dfe138da1..c6cc887655 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt @@ -43,6 +43,7 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* +// Spec: spec/client/chat-list.md#TagListView @Composable fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) { val userTags = remember { chatModel.userTags } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index ed74e083e7..a02e0dc768 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.* private val USER_PICKER_SECTION_SPACING = 32.dp +// Spec: spec/client/chat-list.md#UserPicker @Composable fun UserPicker( chatModel: ChatModel, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index 4827e6ae61..300e5f44fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt @@ -74,6 +74,7 @@ object DatabaseUtils { } } +// Spec: spec/database.md#DBMigrationResult @Serializable sealed class DBMigrationResult { @Serializable @SerialName("ok") object OK: DBMigrationResult() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 1c5f86c8b5..81fac40a40 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -29,7 +29,9 @@ fun DefaultAppBar( onTop: Boolean, showSearch: Boolean = false, searchAlwaysVisible: Boolean = false, + searchPlaceholder: String? = null, onSearchValueChanged: (String) -> Unit = {}, + searchTrailingContent: @Composable (() -> Unit)? = null, buttons: @Composable RowScope.() -> Unit = {}, ) { // If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier @@ -78,7 +80,8 @@ fun DefaultAppBar( AppBar( title = { if (showSearch) { - SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged) + val placeholder = searchPlaceholder ?: stringResource(MR.strings.search_verb) + SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, placeholder = placeholder, trailingContent = searchTrailingContent, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged) } else if (title != null) { title() } else if (titleText.value.isNotEmpty() && connection != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index 7124f34ac0..a122ddd885 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt @@ -10,6 +10,7 @@ import androidx.compose.material.TextFieldDefaults.indicatorLine import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -112,18 +113,26 @@ fun SearchTextField( placeholder = { Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis) }, - trailingIcon = if (searchText.value.text.isNotEmpty()) {{ - IconButton({ - if (alwaysVisible) { - keyboard?.hide() - focusManager.clearFocus() + trailingIcon = if (searchText.value.text.isNotEmpty() || trailingContent != null) {{ + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.offset(x = 8.dp) + ) { + if (searchText.value.text.isNotEmpty()) { + IconButton({ + if (alwaysVisible) { + keyboard?.hide() + focusManager.clearFocus() + } + searchText.value = TextFieldValue("") + onValueChange("") + }) { + Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary) + } } - searchText.value = TextFieldValue(""); - onValueChange("") - }, Modifier.offset(x = reducedCloseButtonPadding)) { - Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,) + trailingContent?.invoke() } - }} else trailingContent, + }} else null, singleLine = true, enabled = enabled, interactionSource = interactionSource, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index db1a0be9da..c4821d1a20 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -114,6 +114,7 @@ fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedStr expect fun SetupClipboardListener() // maximum image file size to be auto-accepted +// Spec: spec/services/files.md#MAX_IMAGE_SIZE const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 @@ -129,6 +130,8 @@ const val MAX_FILE_SIZE_LOCAL: Long = Long.MAX_VALUE expect fun getAppFileUri(fileName: String): URI +expect fun clearImageCaches() + // https://developer.android.com/training/data-storage/shared/documents-files#bitmap expect suspend fun getLoadedImage(file: CIFile?): Pair? @@ -422,6 +425,7 @@ fun deleteAppFiles() { } catch (e: java.lang.Exception) { Log.e(TAG, "Util deleteAppFiles error: ${e.stackTraceToString()}") } + clearImageCaches() } fun directoryFileCountAndSize(dir: String): Pair { // count, size in bytes diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 41f454b2dd..86e51e6937 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -2522,4 +2522,9 @@ البصمة في عنوان الخادم لا تتطابق مع الشهادة: %1$s. لا اشتراك أنت غير متصل بالخادم المستخدم لاستقبال الرسائل من هذا الاتصال (لا يوجد اشتراك). + احذف رسائل العضو + حذف رسائل العضو؟ + احذف الرسائل + ستُحذف رسائل العضو - ولا يمكن التراجع عن ذلك! + أزل واحذف الرسائل diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 371f0e076f..8b1eb44249 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -368,6 +368,18 @@ Edit Info Search + Search images + Search videos + Search voice messages + Search files + Search links + Images + Videos + Voice messages + Files + Links + All messages + Filter Archive Archive report Archive reports @@ -1894,6 +1906,7 @@ Blocked by admin blocked disabled + failed inactive MEMBER Role @@ -1912,6 +1925,7 @@ Group Chat Connection + Connection failed direct indirect (%1$s) Message queue info diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index a3b97f0be2..5c8f73bf93 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -1555,12 +1555,12 @@ Obrint la base de dades… Comproveu que l\'enllaç SimpleX sigui correcte. l\'enviament de fitxers encara no està suportat - Intentant connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte. + Intentant connectar-se al servidor utilitzat per rebre missatges d\'aquesta connexió. S\'està provant de connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte (error: %1$s). Usar perfil actual Usar nou perfil incògnit Error aplicació - Esteu connectat al servidor utilitzat per rebre missatges d\'aquest contacte. + Esteu connectat al servidor utilitzat per rebre missatges d\'aquesta connexió. El teu perfil s\'enviarà al contacte del qual has rebut aquest enllaç. Heu compartit una ruta de fitxer no vàlida. Informeu-ne als desenvolupadors de l\'aplicació. Us connectareu amb tots els membres del grup. @@ -2501,4 +2501,11 @@ L\'empremta digital a l\'adreça del servidor de destinació no coincideix amb el certificat: %1$s. L\'empremta digital a l\'adreça del servidor de reenviament no coincideix amb el certificat: %1$s. L\'empremta digital a l\'adreça del servidor no coincideix amb el certificat: %1$s. + cap subscripció + No esteu connectat(da) al servidor que s\'utilitza per rebre missatges d\'aquesta connexió (sense subscripció). + Suprimir missatges de membre + Suprimir missatges de membre? + Suprimir missatges + Els missatges de membre s\'eliminaran; això no es pot desfer! + Eliminar membre i els seus missatges diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml index 30e557a4e3..38507cc228 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml @@ -859,4 +859,6 @@ Tilslutning af opkald … Tilslutning af opkald Tilslutning (introduceret) + Overfør fra en anden enhed på den nye enhed og scan QR-koden.]]> + Overfør fra en anden enhed diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index e038207801..3254d0a63f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -2612,4 +2612,21 @@ Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein: %1$s. Kein Abonnement Sie sind nicht mit dem Server verbunden, der für den Empfang von Nachrichten dieser Verbindung genutzt wird (kein Abonnement). + Mitgliedsnachrichten löschen + Mitgliedsnachrichten löschen? + Mitgliedsnachrichten löschen + Mitgliedsnachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! + Mitglied entfernen und Nachrichten löschen + Alle Nachrichten + Dateien + Filter + Bilder + Links + Dateien suchen + Bilder suchen + Links suchen + Videos suchen + Sprachnachrichten suchen + Videos + Sprachnachrichten diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml index 9e38019c8b..6f326c33c8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -1,39 +1,39 @@ - 1 μέρα + 1 ημέρα 1 μήνας Για το SimpleX - Σαρώστε τον QR κωδικό + Σάρωσε τον QR κωδικό α + β Για το SimpleX Chat - Σαρώστε τον κωδικό ασφαλείας από την εφαρμογή επαφών σας + Σάρωσε τον κωδικό ασφαλείας από την εφαρμογή επαφών σου Ασφαλή ουρά δε Κωδικός ασφαλείας - Σαρώστε τον κωδικό QR διακομιστή + Σάρωσε τον QR κωδικό του διακομιστή μυστικό 1 εβδομάδα αξιολόγηση ασφαλείας - Συναινώ + Επέτρεψε Αποδοχή Αποδοχή ανώνυμης περιήγησης Προσθήκη προκαθορισμένου διακομιστή Προσθήκη σε άλλη συσκευή - Όλες οι επαφές σας θα παραμείνουν ενεργές. + Όλες οι επαφές σου θα παραμείνουν ενεργές. Αποδοχή διαχειριστής - Προσθέστε μήνυμα καλωσορίσματος + Πρόσθεσε μήνυμα καλωσορίσματος Όλα τα μέλη της ομάδας θα παραμήνουν συνδεδεμένα. Προσθήκη προφίλ - Προφορά + Χρώμα έμφασης πάντα Αποδοχή - Επιτρέψτε τα μηνύματα που εξαφανίζονται μόνο εάν το επιτρέπει η επαφή σας. - Επιτρέψτε στις επαφές σας να διαγράφουν μη αναστρέψιμα τα απεσταλμένα μηνύματα. (24 ώρες) - Επιτρέψτε στις επαφές σας να στέλνουν μηνύματα που εξαφανίζονται. - Επιτρέπονται τα φωνητικά μηνύματα μόνο εάν τα επιτρέπει η επαφή σας. - Επιτρέψτε στις επαφές σας να σας καλέσουν. - Επιτρέψτε στις επαφές σας να στέλνουν φωνητικά μηνύματα. + Επιτρέψτε τα μηνύματα που εξαφανίζονται μόνο εάν το επιτρέπει η επαφή σου. + Επέτρεψε στις επαφές σου να διαγράφουν μη αναστρέψιμα τα απεσταλμένα μηνύματα. (24 ώρες) + Επέτρεψε στις επαφές σου να στέλνουν μηνύματα που εξαφανίζονται. + Επέτρεψε τα φωνητικά μηνύματα μόνο εάν τα επιτρέπει η επαφή σου. + Επέτρεψε στις επαφές σου να σε καλέσουν. + Επέτρεψε στις επαφές σου να στέλνουν φωνητικά μηνύματα. Να επιτρέπεται η αποστολή άμεσων μηνυμάτων στα μέλη. Επιτρέπεται η αποστολή μηνυμάτων που εξαφανίζονται. Επιτρέπεται η αποστολή φωνητικών μηνυμάτων. @@ -41,70 +41,69 @@ Αποδοχή Αποδοχή αιτήματος σύνδεσης; αποδεκτή κλήση - Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση. + Πρόσβαση στους διακομιστές μέσω διαμομιστή μεσολάβησης SOCKS στη θύρα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση. Προσθήκη διακομιστή Προχωρημένες ρυθμίσεις δικτύου Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών. Οι διαχειριστές μπορούν να δημιουργήσουν τους συνδέσμους συμμετοχής σε ομάδες. Όλες οι συνομιλίες και τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί! Όλα τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί! Τα μηνύματα θα διαγραφούν ΜΟΝΟ για εσάς. - Επιτρέψτε τη μη αναστρέψιμη διαγραφή μηνυμάτων μόνο εάν το επιτρέπει η επαφή σας. (24 ώρες) - Επιτρέπονται οι κλήσεις μόνο εάν η επαφή σας τις επιτρέπει. + Επέτρεψε τη μη αναστρέψιμη διαγραφή μηνυμάτων μόνο εάν το επιτρέπει η επαφή σου. (24 ώρες) + Επιτρέπονται οι κλήσεις μόνο εάν η επαφή σου τις επιτρέπει. Επιτρέψτε τη μη αναστρέψιμη διαγραφή των απεσταλμένων μηνυμάτων. (24 ώρες) Να επιτρέπονται τα φωνητικά μηνύματα; Πάντα ενεργό Να χρησιμοποιείται πάντα αναμεταδότη - Αναζήτηση + Αναζήτησε Ανενεργό - "Το προφίλ σας %1$s θα μοιραστεί" - Η SimpleX διεύθυνση σας + Το προφίλ σου %1$s θα διαμοιραστεί + Η SimpleX διεύθυνση σου Αντίγραφο δεδομένων εφαρμογής 5 λεπτά - Θα συνδεθείτε όταν η συσκευή της επαφής σας είναι συνδεμένει, παρακαλώ περιμένετε ή ελέγξτε αργότερα! - Ο ICE διακομιστής σας + Θα συνδεθείς όταν η συσκευή της επαφής σου είναι συνδεδεμένη, παρακαλώ περίμενε ή έλεγξε αργότερα! + Ο ICE διακομιστής σου Έκδοση εφαρμογής - Στείλατε πρόσκληση ομάδας + Έστειλες πρόσκληση ομάδας 1 λεπτό - Ο διακομιστής σας + Ο διακομιστής σου Διεύθυνση Ακύρωση Πίσω 30 δευτερόλεπτα - Θα συνδεθείτε με όλα τα μέλη της ομάδας. + Θα συνδεθείς με όλα τα μέλη της ομάδας. %1$s θέλει να συνδεθεί μαζί σου μέσω - Επιτρέπονται αντιδράσεις μηνύματος. - Ο διακομιστής XFTP σας - Η διεύθυνση του διακομιστή σας - Το προφίλ, επαφές και παραδομένα μηνύματα σας είναι αποθηκευμένα στην συσκευή σας. - Ο διακομιστής SMP σας + Επέτρεψε τις αντιδράσεις σε μηνύματα. + Ο διακομιστής XFTP σου + Η διεύθυνση του διακομιστή σου + Το προφίλ, επαφές και παραδομένα μηνύματα σου είναι αποθηκευμένα στην συσκευή σου. + Ο διακομιστής SMP σου Επιτρέπεται να σταλούν αρχεία και μέσα. - Το τυχαίο προφίλ σας + Το τυχαίο προφίλ σου %1$s ΜΕΛΗ - Επιτρέπεται - Οι προτιμήσεις σας + Αποδοχή + Οι προτιμήσεις σου Συντάκτης - Ο ΙCE διακομιστής σας - Κωδικός εφαρμογής + Ο ΙCE διακομιστής σου + Κωδικός πρόσβασης εφαρμογής Αίτημα σύνδεσης θα σταλεί σε αυτό το μέλος της ομάδας. ΟΙΚΟΝΑ ΕΦΑΡΜΟΓΗΣ Εφαρμογή - Οι ρυθμίσεις σας + Οι ρυθμίσεις σου Έκδοση εφαρμογής: v%s σφάλμα κλήσης - "ακυρώθηκε %s" + ακυρώθηκε %s ακύρωση πρόβλεψη συνδέσμου - Αλλαγή κωδικού πρόσβασης βάση δεδομένων? + Αλλαγή φράσης πρόσβασης της βάσης δεδομένων? Αλλαγή κωδικού πρόσβασης Αλλαγή ρόλου του %s σε %s Φόντο Δεν είναι δυνατή η προετοιμασία της βάσης δεδομένων - Ένα νέο τυχαίο προφίλ θα μοιραστεί. + Ένα νέο τυχαίο προφίλ θα διαμοιραστεί. Δεν είναι δυνατή η πρόσκληση επαφών! Αλλαγή διεύθυνσης λήψης Πιστοποίηση μη διαθέσιμη - Αλλαγή - " -\nΔιαθέσιμο στην έκδοση 5.1" + Άλλαξε + \nΔιαθέσιμο στην έκδοση 5.1 Τέλος κλήσης ΚΛΗΣΕΙΣ Αυτόματη αποδοχή @@ -126,25 +125,25 @@ Όλα τα δεδομένα της εφαρμογής διαγράφηκαν. Εμφάνιση Τέλος κλήσης %1$s - Ακύρωση + Ακύρωσε Αλλαγή διεύθυνσης λήψης; αλλαγή διεύθυνσης… Αλλαγή λειτουργίας αυτοκαταστροφής Κάμερα κλήση σε εξέλιξη Αυτόματη αποδοχή εικόνων - Αλλαγή του ρόλου σας σε %s + Αλλαγή του ρόλου σου σε %s Κλήση σε εξέλιξη Πιστοποίηση απέτυχε - "σύνδεση %1$d" + σύνδεση %1$d Δημιουργία διεύθυνση SimpleX - επαφή έχει κρυπτογράφηση από άκρο σε άκρο + η επαφή έχει κρυπτογράφηση από άκρη-σε-άκρη Δημιουργία μια ομάδας χρησιμοποιώντας ένα τυχαίο προφίλ. Δημιουργία ομάδας Δημιουργία προφίλ Η επαφή και όλα τα μηνύματα θα διαγραφούν - αυτό δεν μπορεί να αναιρεθεί! Δημιουργία προφίλ - Οι επαφές μπορούν να επισημάνουν μηνύματα προς διαγραφή; θα μπορείτε να τα δείτε. + Οι επαφές μπορούν να επισημάνουν μηνύματα προς διαγραφή, τα οποία θα μπορείς να τα δεις. Σύνδεση μέσω μιας εφάπαξ σύνδεσης; Δημιουργία σύνδεσμου Σύνδεση μέσω σύνδεσμο/κωδικό γρήγορης ανταπόκρισης @@ -153,11 +152,11 @@ συνδέεται… Όνομα επαφής Δημιουργία διεύθυνσης - Αντιγραφή + Αντέγραψε Συνέχεια Σύνδεση μέσω σύνδεσμο; Επαφή υπάρχει ήδη - Σύνδεση στον εαυτό σας; + Σύνδεση στον εαυτό σου; Δημιουργία μυστικής ομάδας Συνδεδεμένη σε επιφάνεια εργασίας δημιουργός @@ -180,8 +179,8 @@ Συνδε Σύνδεση ανώνυμης περιήγησης σύνδεση επετεύχθη - επαφή δεν έχει κρυπτογράφηση από άκρο σε άκρο - Επαφή επιτρέπει + η επαφή δεν έχει κρυπτογράφηση από άκρη-σε-άκρη + Η επαφή επιτρέπει σύνδεση (ανακοινώθηκε) Συνδεδεμένος συνδέεται… @@ -194,29 +193,29 @@ Δημιουργία μυστικής ομάδας Σφάλμα σύνδεσης Η επαφή δεν είναι συνδράμει αυτή τη στιγμή! - Συνδεδεμένο απευθείας - Η ιδιωτικότητά σας - Η επαφή σας έστειλε ένα αρχείο το οποίο είναι μεγαλύτερο από το παρόν υποστηριζόμενο μέγεθος (%1$s). - Το προφίλ της συνομιλίας σας θα σταλεί στην επαφή σας + αιτούμενη σύνδεση + Η ιδιωτικότητά σου + Η επαφή σου έστειλε ένα αρχείο το οποίο είναι μεγαλύτερο από το παρόν υποστηριζόμενο μέγεθος (%1$s). + Το προφίλ της συνομιλίας σου θα σταλεί\nστην επαφή σου Χρήση νέου ανώνυμου προφίλ Ήδη συνδέεται - Απορρίψατε την πρόσκληση της ομάδας + Απέρριψες την πρόσκληση της ομάδας Σύνδεση μέσω διεύθυνση επαφής - Χρήση του τρέχων προφίλ - Σύνδεση - Το τρέχον προφίλ σας - αφαιρέσατε %1$s - "Συμμετοχή ομάδας;" - Οι επαφες σας θα παραμένουν συνδεδεμένες. - Προσπάθεια σύνδεσης με τον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτήν την επαφή. + Χρήση του τρέχοντος προφίλ + Συνδέσου + Το τρέχον προφίλ σου + αφαίρεσες %1$s + Συμμετοχή ομάδας; + Οι επαφες σου θα παραμένουν συνδεδεμένες. + Προσπάθεια σύνδεσης με τον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτήν τη σύνδεση. Το προφίλ σου θα σταλεί στην επαφή από την οποία έλαβες αυτόν τον σύνδεσμο. σφάλμα Άνοιγμα βάση δεδομένων… Η προβολή συνετρίβη συνδεδεμένο - Κοινοποιήσατε μια μη έγκυρη διαδρομή αρχείου. Αναφέρετε το πρόβλημα στους προγραμματιστές της εφαρμογής. + Κοινοποίησες μία μη έγκυρη διαδρομή αρχείου. Ανέφερε το πρόβλημα στους προγραμματιστές της εφαρμογής. Μη έγκυρη διαδρομή αρχείου - Είστε συνδεδεμένοι στον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτήν την επαφή. + Είσαι συνδεδεμένος στον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτήν τη σύνδεση. %d μύνημα επισημάνθηκε ως διαγραμμένο %1$d μυνήματα συντονίζονται από %2$s επισημάνθηκε ως διαγραμμένο @@ -230,22 +229,22 @@ Η αλλαγή διεύθυνσης θα ακυρωθεί. Θα χρησιμοποιηθεί η παλιά διεύθυνση παραλαβής. Ενεργές συνδέσεις Προχωρημένες ρυθμίσεις - Πρόσθετος τόνος + Επιπρόσθετο χρώμα έμφασης Προσθήκη επαφής - Διακοπή αλλαγής διεύθυνσης + Ακύρωση αλλαγής διεύθυνσης Προχωρημένες ρυθμίσεις Οι διαχειριστές μπορούν να αποκλείσουν ένα μέλος για όλους. Αναγνωρισμένο παραπάνω, λοιπόν: - Προσθέστε τη διεύθυνση στο προφίλ σας, έτσι ώστε οι επαφές σας να μπορούν να τη μοιραστούν με άλλα άτομα. Το ενημέρωμένο προφίλ θα σταλεί στις επαφές σας. + Πρόσθεσε τη διεύθυνση στο προφίλ σου, έτσι ώστε οι επαφές σου να μπορούν να τη διαμοιραστούν με άλλα άτομα. Το ενημέρωμένο σου προφίλ θα αποσταλεί στις επαφές σου. διαχειριστές Λάθη αναγνώρισης - Προειδοποίηση: το αρχείο θα διαγραφεί.]]> + Προειδοποίηση: το αρχείο αρχειοθέτησης θα διαγραφεί.]]> Υπέρβαση χωρητικότητας - ο παραλήπτης δεν έλαβε μηνύματα που στάλθηκαν προηγουμένως. αποκλεισμένος από τον διαχειριστή Συνομιλίες όλα τα μέλη - Όλες οι επαφές σας θα παραμείνουν ενεργές. Το ανανεωμένο προφίλ σας θα αποσταλεί στις επαφές σας. + Όλες οι επαφές σου θα παραμείνουν ενεργές. Το ανανεωμένο προφίλ σου θα αποσταλεί στις επαφές σου. Να χρησιμοποιείται πάντα ιδιωτική δρομολόγηση. Ένα κενό προφίλ συνομιλίας με το παρεχόμενο όνομα δημιουργείται και η εφαρμογή ανοίγει ως συνήθως. Η βάση δεδομένων της συνομιλίας διαγράφηκε @@ -263,7 +262,7 @@ Η εφαρμογή κρυπτογραφεί νέα τοπικά αρχεία (εκτός απο βίντεο). Καλύτερες ομάδες Γίνεται ήδη συμμετοχή στην ομάδα! - Αρχειοθέτηση και αποστολή + Αρχειοθέτηση και ανέβασμα %1$d διαφορετικό/κα σφάλμα/τα αρχείου/ων. Η υπηρεσία παρασκηνίου λειτουργεί πάντα - οι ειδοποιήσεις θα εμφανίζονται μόλις τα μηνύματα είναι διαθέσιμα. %1$d αρχείο/α ακόμα κατεβαίνουν. @@ -272,10 +271,10 @@ %1$d αρχείο/α δεν κατέβηκε/καν. %1$s μήνυμα/τα δεν προωθήθηκε/καν Προφίλ συνομιλίας - για κάθε προφίλ συνομιλίας που έχετε στην εφαρμογή.]]> - Παρακαλώ σημειώστε: οι αναμεταδότες μηνυμάτων και αρχείων συνδέονται μέσω διακομιστή μεσολάβησης SOCKS. Οι κλήσεις και οι προεπισκοπήσεις συνδέσμων αποστολής χρησιμοποιούν άμεση σύνδεση.]]> + για κάθε προφίλ συνομιλίας που έχεις στην εφαρμογή.]]> + Παρακαλώ σημείωσε: οι αναμεταδότες μηνυμάτων και αρχείων συνδέονται μέσω διακομιστή μεσολάβησης SOCKS. Οι κλήσεις και οι προεπισκοπήσεις συνδέσμων αποστολής χρησιμοποιούν άμεση σύνδεση.]]> Πάντα - Η ενημέρωση της εφαρμογής κατεβαίνει + Η ενημέρωση της εφαρμογής κατέβηκε Έλεγχος για ενημερώσεις Οποιοσδήποτε μπορεί να φιλοξενήσει διακομιστές. κλήση ήχου (χωρίς κρυπτογράφηση e2e) @@ -291,21 +290,21 @@ Χρώματα συνομιλίας ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ Η συνομιλία εκτελείται - Παρακαλώ σημειώστε: ΔΕΝ θα μπορείτε να ανακτήσετε ή να αλλάξετε τη φράση πρόσβασης εάν τη χάσετε.]]> + Παρακαλώ σημείωσε: ΔΕΝ θα μπορείς να ανακτήσεις ή να αλλάξεις τη φράση πρόσβασης εάν τη χάσεις.]]> Αποκλεισμός για όλους - Και εσείς και η επαφή σας μπορείτε να προσθέστε αντιδράσεις μηνυμάτων. - Και εσείς και η επαφή σας μπορείτε να κάνετε κλήσεις. + Και εσύ και η επαφή σου μπορείτε να προσθέστε αντιδράσεις μηνυμάτων. + Και εσύ και η επαφή σου μπορείτε να κάνετε κλήσεις. Επιτρέψτε την αποστολή συνδέσμων SimpleX. Αραβικά, Βουλγαρικά, Φινλανδικά, Εβραϊκά, Ταϊλανδέζικα και Ουκρανικά - χάρη στους χρήστες και το Weblate. Μεταφορά δεδομένων εφαρμογής Θάμπωμα για καλύτερη ιδιωτικότητα. Η συνομιλία έχει μεταφερθεί! Αρχειοθέτηση της βάσης δεδομένων - Όλες οι επαφές, συζητήσεις και αρχεία θα κρυπτογραφηθούν με ασφάλεια και θα μεταφορτωθούν σε διαμορφωμένα κομμάτια αναμετάδοσης XFTP. + Όλες οι επαφές, οι συζητήσεις και τα αρχεία θα κρυπτογραφηθούν με ασφάλεια και θα μεταφορτωθούν τμηματικά σε διαμορφωμένους αναμεταδότες XFTP. Κινητή τηλεφωνία - Δημιουργία ομάδας : για την δημιουργίας νέας ομάδας.]]> - Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά - Συζήτηση με τους προγραμματιστές + Δημιουργία ομάδας : για να δημιουργήσεις μία νέα ομάδα.]]> + Έλεγξε τη σύνδεσή σου στο διαδίκτυο και δοκίμασε ξανά + Συνομίλησε με τους προγραμματιστές Ζήτησε να λάβει το βίντεο Δεν είναι δυνατή η αποστολή μηνυμάτων στο μέλος της ομάδας Αλλαγή λειτουργίας κλειδώματος @@ -313,16 +312,16 @@ άλλαξε η διεύθυνση για εσάς και %d άλλες εκδηλώσεις Μαύρο - Πρόσθετο δευτερεύον - Και εσείς και η επαφή σας μπορείτε να διαγράψετε απεσταλμένα μηνύματα χωρίς ανατροπή. (24 ώρες) - Και εσείς και η επαφή σας μπορείτε να στείλετε ηχητικά μηνύματα. + Επιπρόσθετο δευτερεύων + Και εσύ και η επαφή σου μπορείτε να διαγράψετε απεσταλμένα μηνύματα χωρίς ανατροπή. (24 ώρες) + Και εσύ και η επαφή σου μπορείτε να στείλετε ηχητικά μηνύματα. Η συνομιλία σταμάτησε - Η συνομιλία έχει διακοπεί. Εάν χρησιμοποιήσατε ήδη αυτήν τη βάση δεδομένων σε άλλη συσκευή, θα πρέπει να τη μεταφέρετε πίσω προτού ξεκινήσετε τη συνομιλία. - Η λειτουργία βελτιστοποίησης της μπαταρίας είναι ενεργή, η υπηρεσία παρασκηνίου και τα περιοδικά αιτήματα για νέα μηνύματα θα απενεργοποιηθούν. Μπορείτε να τα ενεργοποιήσετε ξανά μέσω των ρυθμίσεων. - σύνδεσμος μιας χρήσης + Η συνομιλία έχει διακοπεί. Εάν ήδη χρησιμοποίησες αυτήν τη βάση δεδομένων σε άλλη συσκευή, θα πρέπει να τη μεταφέρεις πίσω προτού ξεκινήσεις τη συνομιλία. + Η λειτουργία βελτιστοποίησης της μπαταρίας είναι ενεργή, η υπηρεσία παρασκηνίου και τα περιοδικά αιτήματα για νέα μηνύματα θα απενεργοποιηθούν. Μπορείς να τα ενεργοποιήσεις ξανά μέσω των ρυθμίσεων. + σύνδεσμος 1-χρήσης Κλήσεις ήχου & βίντεο Κλήσεις ήχου/βίντεο - Κωδικός εφαρμογής + Κωδικός πρόσβασης εφαρμογής Συνεδρία εφαρμογής Η συνομιλία σταμάτησε Έλεγχος για ενημερώσεις @@ -331,47 +330,47 @@ Bluetooth έντονο Κονσόλα συνομιλίας - Παρακαλώ σημειώστε: η χρήση της ίδιας βάσης δεδομένων σε δύο συσκευές θα διακόψει την αποκρυπτογράφηση των μηνυμάτων από τις συνδέσεις σας, ως προστασία ασφαλείας.]]> + Παρακαλώ σημείωσε: η χρήση της ίδιας βάσης δεδομένων σε δύο συσκευές θα διακόψει την αποκρυπτογράφηση των μηνυμάτων από τις συνδέσεις σου, ως προστασία ασφαλείας.]]> Χρησιμοποιεί περισσότερη μπαταρία! Η εφαρμογή εκτελείται πάντα στο παρασκήνιο - οι ειδοποιήσεις εμφανίζονται αμέσως.]]> Η βάση δεδομένων της συνομιλίας εξάχθηκε κλήση Κακή διεύθυνση Desktop - Μεταφορά απο άλλη συσκευή στη νέα συσκευή και σαρώστε τον κωδικό QR.]]> + Μεταφορά από άλλη συσκευή στη νέα συσκευή και σάρωσε τον κωδικό QR.]]> Με προφίλ συνομιλίας (προεπιλογή) ή μέσω σύνδεσης (BETA). Κάμερα και μικρόφωνο 6 νέες γλώσσες διεπαφής - Καλό για την μπαταρία. Η εφαρμογή ελέγχει για την παραλαβή μηνυμάτων κάθε 10 λεπτά. Ενδέχεται να χάσετε κλήσεις ή επείγοντα μηνύματα.]]> + Καλό για την μπαταρία. Η εφαρμογή ελέγχει για την παραλαβή μηνυμάτων κάθε 10 λεπτά. Ενδέχεται να χάσεις κλήσεις ή επείγοντα μηνύματα.]]> Επισύναψη - Διακοπή αλλαγής διεύθυνσης; + Ακύρωση αλλαγής διεύθυνσης; Επιλέξτε ένα αρχείο Όλα τα νέα μηνύνματα απο %s θα αποκρυφθούν! Δεν είναι δυνατή η λήψη του αρχείου Πιστοποίηση Όλα τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί! - Ελέγχει νέα μηνύματα κάθε 10 λεπτά για έως και 1 λεπτό + Ελέγχει νέα μηνύματα κάθε 10 λεπτά για εώς και 1 λεπτό Η εφαρμογή μπορεί να λαμβάνει ειδοποιήσεις μόνο όταν εκτελείται, καμία υπηρεσία δεν θα ξεκινήσει στο παρασκήνιο Μπορεί να απενεργοποιηθεί μέσω των ρυθμίσεων – οι ειδοποιήσεις θα εξακολουθούν να εμφανίζονται ενώ η εφαρμογή εκτελείται.]]> - Επιτρέψτε τις επαφές σας να χρησιμοποιούν αντιδράσεις μηνυμάτων. - Και εσείς και η επαφή σας μπορείτε να στείλετε μηνύματα που εξαφανίζονται. + Επέτρεψε στις επαφές σου να χρησιμοποιούν αντιδράσεις μηνυμάτων. + Και εσύ και η επαφή σου μπορείτε να στείλετε μηνύματα που εξαφανίζονται. Κάμερα μη διαθέσιμη Ελέγξτε την διεύθυνση του διακομιστή και δοκιμάστε ξανά. - Επιτρέψτε αντιδράσεις μηνυμάτων εφόσον οι επαφές σας το επιτρέπουν. + Επέτρεψε αντιδράσεις μηνυμάτων εφόσον οι επαφές σου το επιτρέπουν. %1$d μήνυμα/τα παραλήφθηκε/καν. Κλήσεις απογορευμένες! Δεν είναι δυνατή η αποστολή μηνύματος Η κλήση έχει ήδη τερματιστεί! Ο κωδικός πρόσβασης της εφαρμογής αντικαθίσταται με κωδικό πρόσβασης αυτοκαταστροφής. Το Android Keystore θα χρησιμοποιηθεί για την ασφαλή αποθήκευση της φράσης πρόσβασης μετά την επανεκκίνηση της εφαρμογής ή την αλλαγή της φράσης πρόσβασης - θα επιτρέπει τη λήψη ειδοποιήσεων. - Δεν είναι δυνατή η πρόσβαση στο Keystore για αποθήκευση του κωδικού πρόσβασης της βάσης δεδομένων + Δεν είναι δυνατή η πρόσβαση στο Keystore για αποθήκευση του κωδικού της βάσης δεδομένων Αποκλεισμός μέλους Αποκλεισμός μέλους; Προτιμήσεις συνομιλίας Καλύτερα μηνύματα Εφαρμογή - Συνέναιση υποβάθμισης + Συναίνεση υποβάθμισης Κάμερα κλήση ήχου - Αρχειοθετήστε τις επαφές για να συνομιλήσετε αργότερα. + Αρχειοθέτησε τις επαφές για να συνομιλήσεις αργότερα. Όλα τα προφίλ %1$d μηνύμα/τα παραλείφθηκε/καν κακό μήνυμα hash @@ -380,7 +379,7 @@ Κακό αναγνωριστικό μηνύματος ΣΥΝΟΜΙΛΙΕΣ Η βάση δεδεδομένων της συνομιλίας εισάχθηκε - "συμφωνία κρυπτογράφησης για %s…" + συμφωνία κρυπτογράφησης για %s… Να επιτραπούν οι κλήσεις; Αποκλεισμός μέλους για όλους; Κλήσεις ήχου και βίντεο @@ -390,16 +389,16 @@ Καλύτερη εμπειρία χρήστη Δεν είναι δυνατή η κλήση μέλους ομάδας Ζήτησε να λάβει την εικόνα - για κάθε επαφή και μέλος ομάδας .\nΛάβετε υπόψη: εάν έχετε πολλές συνδέσεις, η κατανάλωση της μπαταρίας και της κυκλοφορίας μπορεί να είναι σημαντικά υψηλότερη και ορισμένες συνδέσεις μπορεί να αποτύχουν.]]> - Προσθήκη επαφής : για να δημιουργήσετε έναν νέο σύνδεσμο πρόσκλησης ή να συνδεθείτε μέσω ενός συνδέσμου που λάβατε.]]> - Καλύτερο για τη ζωή της μπαταρίας . Θα λαμβάνετε ειδοποιήσεις μόνο όταν εκτελείται η εφαρμογή (ΧΩΡΙΣ υπηρεσία παρασκηνίου).]]> + για κάθε επαφή και μέλος ομάδας .\nΛάβε υπόψη: εάν έχεις πολλές συνδέσεις, η κατανάλωση της μπαταρίας και της χρήσης ίντερνετ μπορεί να είναι σημαντικά υψηλότερη και ορισμένες συνδέσεις μπορεί να αποτύχουν.]]> + Προσθήκη επαφής : για να δημιουργήσεις ένα νέο σύνδεσμο πρόσκλησης ή να συνδεθείς μέσω ενός συνδέσμου που έλαβες.]]> + Καλύτερο για τη ζωή της μπαταρίας . Θα λαμβάνεις ειδοποιήσεις μόνο όταν εκτελείται η εφαρμογή (ΧΩΡΙΣ υπηρεσία παρασκηνίου).]]> Beta Καλύτερες κλήσεις %1$d σφάλμα/τα αρχείου/ων:\n%2$s - 1 συζήτηση με ένα μέλος + 1 συνομιλία με ένα μέλος 1 αναφορά 1 χρόνος - Σχετικά με χειρηστές + Σχετικά με τους χειριστές Αποδοχή Αποδοχή Αποδοχή ως μέλος @@ -409,32 +408,2117 @@ Αποδοχή αιτήματος επαφής αποδέχτηκε %1$s Αποδεχούμενοι όροι - αποδέχτηκε τη πρόσκληση + αποδέχτηκε την πρόσκληση σε αποδέχτηκε Αποδοχή μέλους Προστέθηκαν διακομιστές πολυμέσων και αρχείων Προστέθηκε διακομιστής μυνημάτων Προσθήκη φίλων - Πρόσθετος τόνος 2 + Επιπρόσθετο χρώμα έμφασης 2 Προσθήκη λίστας Προσθήκη μυνήματος - Διεύθυνση ή σύνδεσμος μιας χρήσης; + Διεύθυνση ή σύνδεσμος 1-χρήσης; Ρυθμίσεις διεύθυνσης Προσθήκη μέλη ομάδας - Προσθήκη στην λίστα + Προσθήκη στη λίστα Πρόσθεσε τα μέλη της ομάδας σου στις συνομιλίες. όλα - Όλα + Όλες Όλες οι συζητήσεις θα διαγραφτούν απο την λίστα %s, και η λίστα θα διαγραφτεί Όλα τα καινούργια μυνήματα από αυτά τα μέλη θα είναι κρυμμένα! Επιτρέψτε τα αρχεία και πολυμέσα μόνο αν η επαφή σου το επιτρέπει. Επιτρέψτε την αναφορά μυνημάτων στους διαχειριστές. - Επιτρέψτε τις επαφές σας να σας στέλνουν αρχεία και πολυμέσα. + Επέτρεψε στις επαφές σου να σου στέλνουν αρχεία και πολυμέσα. Όλες η αναφορές θα αρχειοθετηθούν για εσένα. Όλοι οι διακομιστές Άλλος λόγος Η εφαρμογή πάντα να τρέχει στο παρασκήνιο - Αρχειοθέτηση + Αρχειοθέτησε Αρχειοθέτηση όλων των αναφορών; αρχειοθετημένη αναφορά + 4 νέες γλώσσες διεπαφής + Γραμμές εργαλείων εφαρμογής + αρχειοθετημένη αναφορά από %s + Να αρχειοθετηθούν %d αναφορές; + Αρχειοθέτηση αναφοράς + Αρχειοθέτηση αναφοράς; + Αρχειοθέτηση αναφορών + Ερώτηση + Καλύτερη απόδοση ομάδων + Καλύτερη ιδιωτικότητα και ασφάλεια + Βιογραφικό: + Το βιογραφικό είναι πολύ μεγάλο + Αποκλεισμός μελών για όλους; + Θόλωμα + Μποτ + Εσύ και η επαφή σου, μπορείτε να στέλνετε αρχεία και πολυμέσα. + Διεύθυνση επιχείρησης + Επαγγελματικές συνομιλίες + Επαγγελματική σύνδεση + Επιχειρήσεις + Χρησιμοποιώντας το SimpleX Chat συμφωνείς να:\n- στέλνεις μόνο νόμιμο περιεχόμενο στις δημόσιες ομάδες.\n- σέβεσαι τους άλλους χρήστες – όχι spam. + Δεν μπορείς να αλλάξεις το προφίλ + δεν μπορείς να στείλεις μηνύματα + Καταλανικά, Ινδονησιακά, Ρουμανικά και Βιετναμέζικα – ευχαριστούμε τους χρήστες μας! + με μία μόνο επαφή - προσωπικό διαμοιρασμό ή μέσω οποιασδήποτε εφαρμογής μηνυμάτων.]]> + με κρυπτογράφηση από άκρη-σε-άκρη και με μετα-κβαντική ασφάλεια σε άμεσα μηνύματα.]]> + Επέτρεψε το στο επόμενο παράθυρο διαλόγου για να λαμβάνεις ειδοποιήσεις άμεσα.]]> + Συσκευές Xiaomi: ενεργοποίησε το Αυτόματο Ξεκίνημα στις ρυθμίσεις συστήματος για να λειτουργούν οι ειδοποιήσεις.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s βρίσκεται σε κακή κατάσταση]]> + Σάρωση QR κωδικού.]]> + %s με τον λόγο: %s]]> + σαρώσεις τον QR κωδικό στη βιντεοκλήση, ή η επαφή σου να διαμοιραστεί ένα σύνδεσμο πρόσκλησης.]]> + δείξε τον QR κωδικό στη βιντεοκλήση, ή μοιράσου το σύνδεσμο.]]> + (νέο)]]> + (αυτή η συσκευή v%s)]]> + κρυπτογράφηση από άκρη-σε-άκρη.]]> + κρυπτογράφηση από άκρη-σε-άκρη με πλήρη εμπιστευτικότητα, δυνατότητα απόρριψης και ανάκτηση μετά από παραβίαση.]]> + κβαντο-ανθεκτική κρυπτογράφηση e2e και με πλήρη εμπιστευτικότητα, δυνατότητα απόρριψης και ανάκτηση μετά από παραβίαση.]]> + %s έχει μη υποστηριζόμενη έκδοση. Βεβαιώσου ότι χρησιμοποιείς την ίδια έκδοση και στις δύο συσκευές.]]> + %s είναι απασχολημένο]]> + %s είναι ανενεργό]]> + %s δεν υπάρχει]]> + %s έχει αποσυνδεθεί]]> + %s έχει αποσυνδεθεί]]> + Άνοιγμα στην εφαρμογή κινητού, μετά πάτα Σύνδεση μέσα στην εφαρμογή.]]> + Χρήση από τον υπολογιστή στην εφαρμογή του κινητού και σκάναρε τον QR κωδικό.]]> + Οδηγό Χρήσης.]]> + αποθετήριό μας στο GitHub.]]> + Χρήση .onion hosts σε Όχι, αν ο διακομιστής μεσολάβησης SOCKS δεν τα υποστηρίζει.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %1$s!]]> + %s]]> + Χρήση μπαταρίας εφαρμογής / Απεριόριστη στις ρυθμίσεις της εφαρμογής.]]> + το SimpleX τρέχει στο παρασκήνιο αντί να χρησιμοποιεί ειδοποιήσεις push.]]> + Χρήση μπαταρίας εφαρμογής / Απεριόριστη στις ρυθμίσεις της εφαρμογής.]]> + %s, αποδέξου τους όρους χρήσης.]]> + %1$s.]]> + %1$s.]]> + %1$s.]]> + %1$s.]]> + πρέπει να χρησιμοποιείς την ίδια βάση δεδομένων σε δύο συσκευές.]]> + Άνοιγμα στην εφαρμογή κινητού κουμπί.]]> + συνδεθείς με τους δημιουργούς του SimpleX Chat για να κάνεις ερωτήσεις και να λαμβάνεις ενημερώσεις.]]> + μόνο αφού γίνει αποδεκτό το αίτημά σου.]]> + Να αλλάξεις την αυτόματη διαγραφή μηνυμάτων; + Αλλαγή των προφίλ συνομιλίας + Αλλαγή λίστας + Αλλαγή σειράς + Συνομιλία + Η συνομιλία υπάρχει ήδη! + Συνομιλίες με μέλη + Η συνομιλία θα διαγραφεί για όλα τα μέλη – αυτή η ενέργεια δεν μπορεί να αναιρεθεί! + Η συνομιλία θα διαγραφεί για εσένα – αυτή η ενέργεια δεν μπορεί να αναιρεθεί! + Συνομίλησε με τους διαχειριστές + Συνομίλησε με τους διαχειριστές + Συνομίλησε με τους διαχειριστές + Συνομίλησε με μέλος + Συνομιλία με μέλη, πριν συνενωθούν. + Έλεγχος μηνυμάτων κάθε 10 λεπτά + Τα κομμάτια κατέβηκαν + Τα τμήματα των αρχείων ανέβηκαν + Καθάρισε + Καθαρισμός + Καθαρισμός + Καθαρισμός συνομιλίας + Καθαρισμός συνομιλίας; + Καθαρισμός ιδιωτικών σημειώσεων; + Καθαρισμός επαλήθευσης + Κάνε κλικ στο κουμπί πληροφοριών δίπλα στο πεδίο διεύθυνσης για να επιτρέψεις τη χρήση του μικροφώνου. + Κουμπί κλεισίματος + χρωματισμένο + Λειτουργία χρώματος + Έρχεται σύντομα! + Παράβαση των κατευθυντήριων γραμμών της κοινότητας + Σύγκριση αρχείου + Σύγκρινε τους κωδικούς ασφαλείας με τις επαφές σου. + ολοκληρώθηκε + Ολοκληρωμένο + Οι όροι έγιναν αποδεκτοί στις: %s. + Όροι χρήσης + Οι όροι θα γίνουν αποδεκτοί για τους ενεργούς χειριστές μετά από 30 ημέρες. + Οι όροι θα γίνουν αποδεκτοί στις: %s. + Οι όροι θα γίνουν αυτόματα αποδεκτοί για τους ενεργούς χειριστές στις: %s. + Διαμορφωμένοι SMP διακομιστές + Διαμορφωμένοι XFTP διακομιστές + Διαμορφωμένοι ICE διακομιστές + Διαμόρφωση χειριστών διακομιστή + Επιβεβαίωσε + Επιβεβαίωση διαγραφής επαφής; + Επιβεβαίωση αναβαθμίσεων βάσης δεδομένων + Επιβεβαίωση αρχείων από άγνωστους διακομιστές. + Επιβεβαίωση ρυθμίσεων δικτύου + Επιβεβαίωση νέας φράσης πρόσβασης… + Επιβεβαίωση κωδικού πρόσβασης + Επιβεβαίωση κωδικού + Επιβεβαίωσε ότι θυμάσαι τη φράση πρόσβασης της βάσης δεδομένων για να τη μεταφέρεις. + Επιβεβαίωση μεταφόρτωσης + Επιβεβαίωση των διαπιστευτηρίων σου + σύνδεση + Σύνδεση + Σύνδεση + Σύνδεση + Αυτόματη σύνδεση + Απευθείας σύνδεση; + συνδεδεμένος + συνδεδεμένος + συνδεδεμένος + Συνδεδεμένος + Συνδεδεμένος + Συνδεδεμένος υπολογιστής + Συνδεδεμένοι διακομιστές + Συνδέσου γρηγορότερα! 🚀 + Συνδέεται + κλήση σε σύνδεση… + Σύνδεση κλήσης + σύνδεση (σε εξέλιξη) + συνδέεται (μέσω πρόσκλησης) + Σύνδεση με την επαφή, περίμενε ή δοκίμασε αργότερα! + Κατάσταση σύνδεσης και διακομιστών. + Σύνδεση μπλοκαρισμένη + Η σύνδεση έχει μπλοκαριστεί από τον χειριστή του διακομιστή:\n%1$s. + Η σύνδεση δεν είναι έτοιμη. + Η σύνδεση απαιτεί επαναδιαπραγμάτευση κρυπτογράφησης. + Συνδέσεις + Ασφάλεια σύνδεσης + Η σύνδεση διακόπηκε + Η σύνδεση διακόπηκε + Η σύνδεση με τον υπολογιστή βρίσκεται σε κακή κατάσταση + - σύνδεση με υπηρεσία καταλόγου (δοκιμαστικό)!\n- επιβεβαιώσεις παράδοσης (εώς 20 μέλη).\n- γρηγορότερα και πιο σταθερά. + Συνδέσου με τους φίλους σου πιο γρήγορα. + η επαφή %1$s άλλαξε σε %2$s + Η επαφή ελέγχθηκε + η επαφή διαγράφηκε + Η επαφή διαγράφηκε! + η επαφή απενεργοποιήθηκε + Κρυμμένη επαφή: + Η επαφή διαγράφηκε. + η επαφή δεν είναι έτοιμη + ΑΙΤΗΣΕΙΣ ΕΠΑΦΩΝ ΑΠΟ ΟΜΑΔΕΣ + Επαφές + η επαφή πρέπει να αποδεχτεί… + Η επαφή θα διαγραφεί – αυτή η ενέργεια δεν μπορεί να αναιρεθεί! + Το περιεχόμενο παραβιάζει τους όρους χρήσης + Εικονίδιο περιεχομένου + Συνέχεια + Συνέχεια + Συνεισφορά + Έλεγξε το δίκτυό σου + Η συζήτηση διαγράφηκε! + Αντιγράφηκε στο πρόχειρο + Σφάλμα αντιγραφής + Έκδοση πυρήνα: v%s + Γωνία + Δημιουργία + Δημιουργία συνδέσμου 1-χρήσης + Δημιούργησε μια διεύθυνση για να μπορούν οι άλλοι να συνδεθούν μαζί σου. + Δημιουργήθηκε + Δημιουργήθηκε στις + Δημιουργήθηκε στις: %s + Δημιουργία λίστας + Δημιουργία νέου προφίλ στην εφαρμογή υπολογιστή. 💻 + Δημιουργία συνδέσμου 1-χρήσης + Δημιουργία ουράς + Δημιούργησε τη διεύθυνσή σου + Δημιουργία συνδέσμου αρχειοθέτησης + Δημιουργία συνδέσμου… + Κρίσιμο σφάλμα + (τρέχον) + Το κείμενο των τρεχουσών προϋποθέσεων δεν φορτώθηκε, μπορείς να τις δεις μέσω αυτού του συνδέσμου: + Το μέγιστο υποστηριζόμενο μέγεθος αρχείου αυτήν τη στιγμή είναι %1$s. + Τρέχων κωδικός πρόσβασης + Τρέχουσα φράση πρόσβασης… + Τρέχον προφίλ + προσαρμοσμένος + Προσαρμόσιμη μορφή μηνύματος. + Προσάρμοσε και μοίρασε τα θέματα χρωμάτων. + Προσαρμογή θέματος + Προσαρμοσμένα θέματα + Προσαρμοσμένος χρόνος + Σκούρο + Σκούρο + Σκοτεινή λειτουργία + Χρώματα σκοτεινής λειτουργίας + Σκούρο θέμα + Υποβάθμιση βάσης δεδομένων + Η βάση δεδομένων κρυπτογραφήθηκε! + Η φράση πρόσβασης για την κρυπτογράφηση της βάσης δεδομένων θα ενημερωθεί. + Η φράση πρόσβασης για την κρυπτογράφηση της βάσης δεδομένων θα ενημερωθεί και θα αποθηκευτεί στις ρυθμίσεις. + Η φράση κρυπτογράφησης της βάσης δεδομένων θα ενημερωθεί και θα αποθηκευτεί στο Keystore. + Σφάλμα βάσης δεδομένων + Αναγνωριστικό βάσης δεδομένων + Αναγνωριστικό βάσης δεδομένων: %d + Αναγνωριστικά βάσης δεδομένων και επιλογή απομόνωσης μεταφοράς. + Η βάση δεδομένων είναι κρυπτογραφημένη με τυχαία φράση πρόσβασης. Παρακαλώ άλλαξέ την πριν την εξαγωγή. + Η βάση δεδομένων είναι κρυπτογραφημένη με τυχαία φράση πρόσβασης, μπορείς να την αλλάξεις. + Η μετεγκατάσταση της βάσης δεδομένων βρίσκεται σε εξέλιξη.\nΜπορεί να χρειαστούν λίγα λεπτά. + Φράση πρόσβασης βάσης δεδομένων + Φράση πρόσβασης βάσης δεδομένων και εξαγωγή αυτής + Η φράση πρόσβασης της βάσης δεδομένων διαφέρει από αυτή που έχει αποθηκευτεί στο Keystore. + Η φράση πρόσβασης της βάσης δεδομένων απαιτείται για να ανοίξεις τη συνομιλία. + Αναβάθμιση βάσης δεδομένων + Η έκδοση της βάσης δεδομένων είναι νεότερη από την εφαρμογή, αλλά δεν υπάρχει δυνατότητα υποβάθμισης για: %s + Η βάση δεδομένων θα κρυπτογραφηθεί. + Η βάση δεδομένων θα κρυπτογραφηθεί και η φράση πρόσβασης θα αποθηκευτεί στις ρυθμίσεις. + Η βάση δεδομένων θα κρυπτογραφηθεί και η φράση πρόσβασης θα αποθηκευτεί στο Keystore. + ημέρες + %d συνομιλία/ες + %d συνομιλίες με μέλη + %d επαφή/ές επιλέχθηκε/καν + %dη + %d ημέρα + %d ημέρες + Αποστολή για αποσφαλμάτωση + Αποκεντρωμένο + Σφάλμα αποκωδικοποίησης + Σφάλμα αποκρυπτογράφησης + σφάλματα αποκρυπτογράφησης + προεπιλογή (%s) + προεπιλογή (%s) + Διέγραψε + Διαγραφή + Διαγραφή + Διαγραφή + Διαγραφή διεύθυνσης + Διαγραφή διεύθυνσης; + Διαγραφή μετά + Διαγραφή όλων των αρχείων + Διαγραφή και ειδοποίηση επαφής + Διαγραφή συνομιλίας + Διαγραφή συνομιλίας + Διαγραφή συνομιλίας; + Διαγραφή μηνυμάτων συνομιλίας από τη συσκευή σου. + Διαγραφή προφίλ συνομιλίας + Διαγραφή προφίλ συνομιλίας; + Διαγραφή προφίλ συνομιλίας; + Διαγραφή προφίλ συνομιλίας για + Διαγραφή συνομιλίας με μέλος; + Διαγραφή επαφής + Διαγραφή επαφής; + Διαγράφηκε + Διαγράφηκε στις + Διαγραφή βάσης δεδομένων + Διαγραφή βάσης δεδομένων από αυτήν τη συσκευή + Διαγράφηκε στις: %s + διεγραμμένη επαφή + διεγραμμένη ομάδα + Διαγραφή %d μηνυμάτων; + Διαγραφή %d μηνυμάτων μελών; + Διαγραφή αρχείου + Διαγραφή μηνυμάτων και πολυμέσων; + Διαγραφή μηνυμάτων για όλα τα προφίλ συνομιλίας + Διαγραφή για όλους + Διαγραφή για μένα + Διαγραφή ομάδας + Διαγραφή ομάδας; + Διαγραφή εικόνας + Διαγραφή συνδέσμου + Διαγραφή συνδέσμου; + Διαγραφή λίστας; + Διαγραφή μηνύματος μέλους; + Διαγραφή μηνυμάτων μέλους + Διαγραφή μηνυμάτων μέλους; + Διαγραφή μηνύματος; + Διαγραφή μηνυμάτων + Διαγραφή μηνυμάτων + Διαγραφή μηνυμάτων μετά + Διαγραφή ή διαχείριση εώς 200 μηνυμάτων. + Να διαγραφεί η εκκρεμής σύνδεση; + Διαγραφή προφίλ + Διαγραφή ουράς + Διαγραφή αναφοράς + Διαγραφή διακομιστή + Διαγραφή εώς 20 μηνυμάτων ταυτόχρονα. + Διαγραφή χωρίς ειδοποίηση + Σφάλματα διαγραφής + Παράδοση + Επιβεβαιώσεις παράδοσης! + Οι επιβεβαιώσεις παράδοσης είναι απενεργοποιημένες! + Απαρχαιωμένες επιλογές + Περιγραφή + Περιγραφή πολύ μεγάλη + Υπολογιστής + Διεύθυνση υπολογιστή + Η έκδοση της εφαρμογής για υπολογιστή %s δεν είναι συμβατή με αυτήν την εφαρμογή. + Συσκευές υπολογιστή + Ο υπολογιστής έχει μη υποστηριζόμενη έκδοση. Βεβαιώσου ότι χρησιμοποιείς την ίδια έκδοση και στις δύο συσκευές. + Ο υπολογιστής έχει λάθος κωδικό πρόσκλησης + Ο υπολογιστής είναι απασχολημένος + Ο υπολογιστής είναι ανενεργός + Ο υπολογιστής έχει αποσυνδεθεί + Η διεύθυνση διακομιστή προορισμού %1$s δεν είναι συμβατή με τις ρυθμίσεις του διακομιστή προώθησης %2$s. + Σφάλμα διακομιστή προορισμού: %1$s + Η έκδοση του διακομιστή προορισμού %1$s δεν είναι συμβατή με τον διακομιστή προώθησης %2$s. + Αναλυτικά στατιστικά + Λεπτομέρειες + Επιλογές προγραμματιστή + Εργαλεία προγραμματιστή + ΣΥΣΚΕΥΗ + Η επαλήθευση συσκευής είναι απενεργοποιημένη. Απενεργοποιείται το SimpleX Lock. + Η επαλήθευση συσκευής δεν είναι ενεργοποιημένη. Μπορείς να ενεργοποιήσεις το SimpleX Lock από τις Ρυθμίσεις, αφού πρώτα ενεργοποιήσεις την επαλήθευση συσκευής. + Συσκευές + %d αρχείο/α με συνολικό μέγεθος %s + %d συμβάντα ομάδας + %dω + %dώρα + %dώρες + διαφορετική μετεγκατάσταση στην εφαρμογή/βάση δεδομένων: %s / %s + Διαφορετικά ονόματα, avatar και απομόνωση μεταφοράς. + άμεσα + Άμεσα μηνύματα + Τα απευθείας μηνύματα μεταξύ των μελών, είναι απαγορευμένα. + Τα απευθείας μηνύματα μεταξύ των μελών, είναι απαγορευμένα σε αυτή τη συνομιλία + Τα απευθείας μηνύματα μεταξύ των μελών, είναι απαγορευμένα σε αυτήν την ομάδα. + Απενεργοποίηση + Απενεργοποίηση + Απενεργοποίηση αυτόματης διαγραφής μηνυμάτων; + απενεργοποιημένο + απενεργοποιημένο + Απενεργοποιημένο + Απενεργοποίηση διαγραφής μηνυμάτων + Απενεργοποίηση για όλους + Απενεργοποίηση για όλες τις ομάδες + πενεργοποίηση (διατήρηση παρακάμψεων ομάδας) + Απενεργοποίηση (διατήρηση παρακάμψεων) + Απενεργοποίηση ειδοποιήσεων + Απενεργοποίηση αναφορών παράδοσης; + Απενεργοποιίηση αναφορών παράδοσης για τις ομάδες; + Απερνεργοποίση SimpleX Lock + Μήνυμα που εξαφανίζεται + Μηνύματα που εξαφανίζονται + Μηνύματα που εξαφανίζονται + Είναι απαγορευμένα τα μηνύματα που εξαφανίζονται. + Είναι απαγορευμένα τα μηνύματα που εξαφανίζονται σε αυτήν τη συνομιλία. + Να εξαφανιστεί σε + Να εξαφανιστεί σε: %s + Αποσύνδεση + Αποσύνδεση + Αποσύνδεση υπολογιστή; + Αποσυνδεδεμένος + Αποσυνδεδεμένος με το λόγο: %s + Αποσύνδεση τηλεφώνων + Ανιχνεύσιμο μέσω τοπικού δικτύου + Ανακάλυψε και συνδέσου σε ομάδες + Ανακάλυψη μέσω τοπικού δικτύου + Το εμφανιζόμενο όνομα δεν μπορεί να περιέχει κενά. + %dμ + %dμηνύματα + %dμηνύματα μπλοκαρισμενα από το διαχειριστή + %d λπτ + %d λεπτά + %dμήνας + %d μήνες + %dμν + Να μη σταλεί το ιστορικό σε νέα μέλη. + ΜΗΝ στέλνεις μηνύματα απευθείας, ακόμα κι αν ο δικός σου διακομιστής ή ο διακομιστής προορισμού δεν υποστηρίζει ιδιωτική δρομολόγηση. + Μην χρησιμοποιείς διαπιστευτήρια με το διακομιστή μεσολάβησης (proxy). + ΜΗΝ χρησιμοποιείς ιδιωτική δρομολόγηση. + Μην δημιουργήσεις διεύθυνση + Μην ενεργοποιήσεις + Μην χάσεις σημαντικά μηνύματα. + Να μην εμφανιστεί ξανά + Υποβάθμιση και άνοιγμα συνομιλίας + Κατέβασμα + Κατέβασμα + Κατέβηκε + Κατεβασμένα αρχεία + Σφάλματα λήψης + Η λήψη απέτυχε + Λήψη αρχείου + Η αναβάθμιση εφαρμογής βρίσκεται σε εξέλιξη, μην κλείσεις την εφαρμογή + Λήψη αρχείου αρχειοθέτησης + Λήψη λεπτομερειών συνδέσμου + Κατέβασε νέες εκδόσεις από το GitHub. + Κατέβασμα %s (%s) + %d αναφορές + %dδ + %dδευτ + %dδευτερόλεπτα + Διπλότυπο εμφανιζόμενο όνομα! + διπλότυπο μήνυμα + διπλότυπα + %dε + %d εβδομάδα + %d εβδομάδες + e2e κρυπτογραφημένο + e2e κρυπτογραφημένη φωνητική κλήση + e2e κρυπτογραφημένη βιντεοκλήση + Ακουστικό + Επεξεργάσου + Επεξεργασία + επεξεργάστηκε + Επεξεργασία προφίλ ομάδας + Επεξεργασία εικόνας + Email + Ενεργοποίηση + Ενεργοποίηση αυτόματης διαγραφής μηνυμάτων + Ενεργοποίηση φωνητικών κλήσεων από την οθόνη κλειδώματος μέσω των Ρυθμίσεων + Ενεργοποίηση πρόσβασης κάμερας + ενεργοποιημένο + Ενεργοποιημένο για + ενεργοποιημένο για την επαφή + ενεργοποιημένο για εσένα + Ενεργοποίηση μηνυμάτων που εξαφανίζονται από προεπιλογή. + Ενεργοποίησε το Flux στις ρυθμίσεις Δικτύου & διακομιστών για καλύτερη προστασία μεταδεδομένων. + Ενεργοποίηση για όλα + Ενεργοποίηση για όλες τις ομάδες + Ενεργοποίηση στις άμεσες συνομιλίες (ΔΟΚΙΜΑΣΤΙΚΟ)! + Ενεργοποίηση (διατήρηση παρακάμψεων ομάδας) + Ενεργοποίηση (διατήρηση παρακάμψεων) + Ενεργοποίηση κλειδώματος + Ενεργοποίηση αρχείων καταγραφής δραστηριότητας + Ενεργοποίηση αναφορών παράδοσης; + Ενεργοποίηση αναφορών παράδοσης για τις ομάδες; + Ενεργοποίηση αυτοκαταστροφής + Ενεργοποίηση κωδικού αυτοκαταστροφής + Ενεργοποίηση SImpleX Lock + Ενεργοποίηση TCP keep-alive + Κρυπτογράφηση + Κρυπτογράφηση βάσης δεδομένων; + Κρυπτογραφημένη βάση δεδομένων + κρυπτογράφηση συμφωνήθηκε + κρυπτογράφηση συμφωνήθηκε για %s + κρυπτογράφηση οκ + κρυπτογράφηση οκ για %s + επιτρέπεται επαναδιαπραγμάτευση κρυπτογράφησης + επιτρέπεται επαναδιαπραγμάτευση κρυπτογράφησης για %s + Σφάλμα κατά την επαναδιαπραγμάτευση κρυπτογράφησης + Αποτυχία κατά την επαναδιαπραγμάτευση κρυπτογράφησης + Επαναδιαπραγμάτευση κρυπτογράφησης σε εξέλιξη. + απαιτείται επαναδιαπραγμάτευση κρυπτογράφησης + απαιτείται επαναδιαπραγμάτευση κρυπτογράφησης για %s + Κρυπτογράφηση τοπικών αρχείων + Κρυπτογράφηση αποθηκευμένων αρχείων & πολυμέσων + Τερματισμός κλήσης + τερματίστηκε + Εισήγαγε σωστή φράση πρόσβασης. + Εισήγαγε όνομα ομάδας: + Εισήγαγε κωδικό πρόσβασης + Εισήγαγε φράση πρόσβασης + Εισήγαγε φράση πρόσβασης… + Εισήγαγε κωδικό στην αναζήτηση + Χειροκίνητη εισαγωγή διακομιστή + Εισήγαγε το όνομα συσκευής… + Εισήγαγε το μήνυμα καλωσορίσματος… + Εισήγαγε το μήνυμα καλωσορίσματος… (προαιρετικό) + Εισήγαγε το όνομά σου: + Σφάλμα + Σφάλμα + Σφάλμα + Σφάλμα + Σφάλμα: %1$s + Σφάλμα κατά την ακύρωση αλλαγής διεύθυνσης + Σφάλμα κατά την αποδοχή των όρων + Σφάλμα κατά την αποδοχή του αιτήματος επαφής + Σφάλμα κατά την αποδοχή μέλους + Επιδιόρθωση σύνδεσης; + Επιδιόρθωση σύνδεσης; + Διόρθωσε την κρυπτογράφηση μετά από επαναφορά αντιγράφων ασφαλείας. + Η επιδιόρθωση δεν υποστηρίζεται από την επαφή + Η επιδιόρθωση δεν υποστηρίζεται από μέλος ομάδας + Αντιστροφή κάμερας + Μέγεθος γραμματοσειράς + Για όλους τους διαχειριστές + για καλύτερη ιδιωτικότητα μεταδεδομένων + Για το προφίλ συνομιλίας %s: + ΓΙΑ ΚΟΝΣΟΛΑ + Για όλους + Για παράδειγμα, αν η επαφή σου λαμβάνει μηνύματα μέσω κάποιου SimpleX Chat διακομιιστή, η εφαρμογή σου θα τα παραδίδει μέσω ενός Flux διακομιστή. + Για μένα + Για ιδιωτική δρομολόγηση + Για μέσα κοινωνικής δικτύωσης + Προώθηση + Προώθηση %1$s μηνύματος/ων; + Προώθηση και αποθήκευση μηνυμάτων + προωθήθηκε + Προωθήθηκε + Προωθήθηκε από + Προώθηση %1$s μηνυμάτων + Διακομιστής προώθησης: %1$s\nΣφάλμα διακομιστή προορισμού: %2$s + Διακομιστής προώθησης: %1$s\nΣφάλμα: %2$s + Ο διακομιστής προώθησης %1$s δεν κατάφερε να συνδεθεί με τον διακομιστή προορισμού %2$s. Δοκίμασε ξανά αργότερα. + Η διεύθυνση του διακομιστή προώθησης είναι ασύμβατη με τις ρυθμίσεις του δικτύου: %1$s. + Η έκδοση του διακομιστή προώθησης είναι ασύμβατη με τις ρυθμίσεις του δικτύου: %1$s. + Προώθηση μηνύματος… + Προώθηση μηνυμάτων… + Προώθηση μηνυμάτων χωρίς τα αρχεία; + Προώθησε μέχρι και 20 μηνύματα μαζί. + Βρέθηκε υπολογιστής + Διεπαφή στα γαλλικά + Από τη Συλλογή + Πλήρης σύνδεσμος + Πλήρης σύνδεσμος + Πλήρες όνομα: + Πλήρως αποκεντρωμένο – ορατό μόνο στα μέλη. + Περαιτέρω μειωμένη κατανάλωση μπαταρίας + Λάβε ειδοποίηση όταν σε αναφέρουν. + Καλησπέρα! + Καλημέρα! + Χορήγησε στις ρυθμίσεις + Χορήγησε άδειες + Χορήγησε άδεια/ες για να κάνεις φωνητικές κλήσεις + Ομάδα + Ομάδα + Η ομάδα υπάρχει ήδη! + η ομάδα διαγράφηκε + Πλήρες όνομα ομάδας: + Ανενεργή ομάδα + Έληξε η πρόσκληση ομάδας + Η πρόσκληση για την ομάδα δεν ισχύει πια, αφαιρέθηκε από τον αποστολέα. + η ομάδα διαγράφηκε + Σύνδεσμος ομάδας + Σύνδεσμοι ομάδας + Διαχείριση ομάδας + Η ομάδα δεν βρέθηκε! + Προτιμήσεις ομάδας + Το προφίλ της ομάδας αποθηκεύεται στις συσκευές των μελών και όχι στους διακομιστές. + το προφίλ της ομάδας ανανεώθηκε + Ομάδες + Μήνυμα καλωσορίσματος ομάδας + Η ομάδα θα διαγραφεί για όλα τα μέλη – αυτή η ενέργεια δεν μπορεί να αναιρεθεί! + Η ομάδα θα διαγραφεί για σένα – αυτή η ενέργεια δεν μπορεί να αναιρεθεί! + Τερματισμός κλήσης + Ακουστικά + βοήθεια + ΒΟΗΘΕΙΑ + Βοήθησε τους διαχειριστές να διαχειρίζονται τις ομάδες τους. + Γεια σου!\nΣυνδέσου μαζί μου μέσω SimpleX Chat: %s + Κρυφό + Κρυμμένα προφίλ συνομιλίας + Κρυφό συνθηματικό προφίλ + Κρύψε + Κρύψε + Κρύψε + Κρύψε: + Απόκρυψη της οθόνης της εφαρμογής στις πρόσφατες εφαρμογές. + Απόκρυψη επαφής και μηνύματος + Απόκρυψη προφίλ + Ιστορικό + Το ιστορικό δεν αποστέλλεται σε νέα μέλη. + Φιλοξένησε + ώρες + Πως επηρεάζει τη μπαταρία + Πως βοηθάει την ιδιωτικότητα + Πως δουλεύει + Πως δουλεύει το SimpleX + Πως να + Πως να το χρησιμοποιήσεις + Πως να χρησιμοποιήσεις markdown σύνταξη + Πως να χρησιμοποιήσεις τους διακομιστές σου + Διεπαφή στα Ουγγρικά και Τουρκικά + ICE διακομιστές (ένας σε κάθε γραμμή) + Αν δεν μπορείς να συναντηθείς προσωπικά, δείξε τον QR κωδικό σε μια βιντεοκλήση ή μοιράσου τον σύνδεσμο. + Αν επιλέξεις να απορρίψεις, ο αποστολέας ΔΕΝ θα ειδοποιηθεί. + Αν επιβεβαιώσεις, οι διακομιστές μηνυμάτων θα μπορούν να δουν τη διεύθυνση IP σου, και ο πάροχός σου – σε ποιους διακομιστές συνδέεσαι. + Αν εισάγεις αυτόν τον κωδικό κατά το άνοιγμα της εφαρμογής, όλα τα δεδομένα της εφαρμογής θα διαγραφούν οριστικά! + Αν εισάγεις τον κωδικό αυτοκαταστροφής κατά το άνοιγμα της εφαρμογής: + Αν έλαβες σύνδεσμο πρόσκλησης για SimpleX Chat, μπορείς να τον ανοίξεις στον περιηγητή σου: + Αγνόησε + Εικόνα + Εικόνα + Η εικόνα αποθηκεύτηκε στη Συλλογή + Η εικόνα στάλθηκε + Η εικόνα θα ληφθεί όταν η επαφή σου ολοκληρώσει τη μεταφόρτωσή της. + Η εικόνα θα ληφθεί όταν η επαφή σου είναι συνδεδεμένη, περίμενε ή έλεγξε αργότερα! + Άμεσα + Ανοσοποιημένο στο spam + Εισαγωγή + Εισαγωγή βάσης δεδομένων συνομιλίας; + Εισαγωγή βάσης δεδομένων + Η εισαγωγή απέτυχε + Εισαγωγή αρχείου αρχειοθέτησης + Εισαγωγή θέματος + Σφάλμα εισαγωγής θέματος + Βελτιωμένη πλοήγηση συνομιλίας + Βελτιωμένη παράδοση μηνύματος + Βελτιωμένη παράδοση μηνύματος + Βελτιωμένη ιδιωτικότητα και ασφάλεια + Βελτιωμένη διαμόρφωση διακομιστή + ανενεργό + Ακατάλληλο περιεχόμενο + Ακατάλληλο προφίλ + Ήχοι κατά τη διάρκεια κλήσης + Ανώνυμο + Ανώνυμες ομάδες + Ανώνυμη λειτουργία + Η ανώνυμη λειτουργία προστατεύει το απόρρητό σου χρησιμοποιώντας ένα νέο τυχαίο προφίλ για κάθε επαφή. + ανώνυμα μέσω συνδέσμου διεύθυνσης επαφής + ανώνυμα μέσω συνδέσμου ομάδας + ανώνυμα μέσω συνδέσμου 1-χρήσης + Εισερχόμενη φωνητική κλήση + Εισερχόμενη βιντεοκλήση + Ασύμβατη έκδοση βάσης δεδομένων + Ασύμβατη έκδοση + Λανθασμένος κωδικός πρόσβασης + Λανθασμένος κωδικός ασφάλειας! + Μεγένθυση γραμματοσειράς + έμμεσο (%1$s) + Πληροφορίες + Αρχικός ρόλος + Για να συνεχίσεις, η συνομιλία θα πρέπει να σταματήσει. + Σε απάντηση του + Εγκαταστάθηκε επιτυχημένα + Εγκατέστησε το SimpleX Chat για το τερματικό + Εγκατάσταση αναβάθμισης + Άμεσα + Άμεσες ειδοποιήσεις + Άμεσες ειδοποιήσεις! + Οι άμεσες ειδοποιήσεις είναι απενεργοποιημένες! + ΧΡΩΜΑΤΑ ΔΙΕΠΑΦΗΣ + Εσωτερικό σφάλμα + μη έγκυρη συνομιλία + Μη έγκυρος σύνδεσμος + μη έγκυρα δεδομένα + Μη έγκυρο εμφανιζόμενο όνομα! + Μη έγκυρος σύνδεσμος + Μη έγκυρος σύνδεσμος + Μη έγκυρος σύνδεσμος! + μη έγκυρη διαμόρφωση μηνύματος + Μη έγκυρη επιβεβαίωση μετεγκατάστασης + Μη έγκυρο όνομα! + Μη έγκυρος QR κωδικός + Μη έγκυρος QR κωδικός + Μη έγκυρη διεύθυνση διακομιστή! + Η πρόσκληση έληξε! + πρόσκληση στην ομάδα %1$s + Προσκάλεσε + Προσκάλεσε + προσκαλεσμένος + προσκεκλημένος %1$s + προσκεκλημένος για σύνδεση + προσκεκλημένος μέσω του συνδέσμου της ομάδας σου + Προσκάλεσε φίλους + Προσκάλεσε μέλη + Προσκάλεσε μέλη + Προσκάλεσε για συνομιλία + Προσκάλεσε σε ομάδα + Οριστική διαγραφή μηνύματος + Η οριστική διαγραφή μηνύματος απαγορεύεται. + Η οριστική διαγραφή μηνύματος απαγορεύεται σε αυτή τη συνομιλία. + Διεπαφή στα Ιταλικά + πλάγια γραφή + Σου επιτρέπει να έχεις πολλές ανώνυμες συνδέσεις χωρίς κοινά δεδομένα μεταξύ τους σε ένα μόνο προφίλ συνομιλίας. + Μπορεί να συμβεί όταν:\n1. Τα μηνύματα λήγουν στον αποστολέα μετά από 2 ημέρες ή στον διακομιστή μετά από 30 ημέρες.\n2. Η αποκρυπτογράφηση μηνύματος απέτυχε, επειδή εσύ ή η επαφή σου χρησιμοποιήσατε παλιό αντίγραφο ασφαλείας της βάσης δεδομένων.\n3. Η σύνδεση έχει παραβιαστεί. + Μπορεί να συμβεί όταν εσύ ή η σύνδεσή σου χρησιμοποιήσατε παλιό αντίγραφο ασφαλείας της βάσης δεδομένων. + Προστατεύει τη διεύθυνση IP και τις συνδέσεις σου. + Διεπαφή στα Ιαπωνικά και Πορτογαλικά + Συμμετοχή + Συμμετοχή ως %s + Συμμετοχή στην ομάδα + Συμμετοχή στην ομάδα; + Συμμετοχή στις συνομιλίες της ομάδας + Ανώνυμη συμμετοχή + Συμμετοχή στη ομάδα + Θέλεις να συμμετάσχεις στην ομάδα σου; + χ + Διατήρηση + Διατήρηση συνομιλίας + Διατήρηση αχρησιμοποίητων προσκλήσεων; + Διατήρησε καθαρές τις συνομιλίες σου + Διατήρησε τις συνδέσεις + Σφάλμα κλειδιού + Μεγάλο αρχείο! + Μάθε περισσότερα + Αποχώρησε + Αποχώρησε από τη συνομιλία + Αποχώρηση από τη συνομιλία; + Αποχώρησε από την ομάδα + Αποχώρηση από την ομάδα; + αποχώρησε + αποχώρησε + Λιγότερη κίνηση στα δίκτυα κινητής τηλεφωνίας. + Ας μιλήσουμε στο SimpleX Chat + Ανοιχτόχρωμο + Ανοιχτόχρωμο + Ανοιχτόχρωμη λειτουργία + Σύνδεσε ένα τηλέφωνο + Επιλογές συνδεδεμένου υπολογιστή + Συνδεδεμένοι υπολογιστές + Συνδεδεμένα τηλέφωνα + Σύνδεσε τις εφαρμογές κινητού και υπολογιστή! 🔗 + εικόνα προεπισκόπησης συνδέσμου + Λίστα + Όνομα λίστας... + Το όνομα της λίστας και το emoji πρέπει να είναι διαφορετικά για όλες τις λίστες. + Διεπαφή στα Λιθουανικά + ΖΩΝΤΑΝΑ + Ζωντανό μήνυμα! + Ζωντανά μηνύματα! + Φόρτωση συνομιλιών… + Φόρτωση προφίλ… + Φόρτωση αρχείου + Τοπικό όνομα + Μόνο τοπικά δεδομένα προφίλ + Κλείδωμα μετά + Λειτουργία κλειδώματος + Σύνδεση χρησιμοποιώντας τα στοιχεία σου + Δημιούργησε μία ιδιωτική συνομιλία + Εξαφάνισε ένα μήνυμα + Κάνε το προφίλ ιδιωτικό! + Βεβαιώσου ότι έχεις σωστή διαμόρφωση του διακομιστή μεσολάβησης. + Βεβαιώσου ότι οι διευθύνσεις του διακομιστή SMP έχουν σωστή μορφή, διαχωρίζονται με νέα γραμμή και δεν είναι διπλότυπες. + Βεβαιώσου ότι το αρχείο έχει σωστή σύνταξη YAML. Κάνε εξαγωγή ενός θέματος για να έχεις παράδειγμα της δομής αρχείου των θεμάτων. + "Βεβαιώσου ότι οι διευθύνσεις των διακομιστών WebRTC ICE έχουν σωστή μορφή, διαχωρίζονται με νέα γραμμή και δεν είναι διπλότυπες." + Βεβαιώσου ότι οι διευθύνσεις των διακομιστών XFTP έχουν σωστή μορφή, διαχωρίζονται με νέα γραμμή και δεν είναι διπλότυπες. + Κάνε τις συνομιλίες σου να ξεχωρίζουν! + Βοήθεια στη Markdown σύνταξη + Σύνταξη Markdown στα μηνύματα + Επισήμανση ως αναγνωσμένο + Επισήμανση ως μη αναγνωσμένο + Επισήμανση ως επαληθευμένο + Μέχρι 40 δευτερόλεπτα, λαμβάνεται άμεσα. + Διακομιστές πολυμέσων & αρχείων + Μεσαίο + μέλος + ΜΕΛΟΣ + Μέλος %1$s + το μέλος %1$s άλλαξε σε %2$s + Εγγραφή μέλους + το μέλος έχει παλαιότερη έκδοση + Το μέλος είναι ανενεργό + Το μέλος διαγράφηκε – δεν μπορεί να γίνει αποδοχή του αιτήματος + Τα μηνύματα του μέλους θα διαγραφούν – αυτό δεν μπορεί να αναιρεθεί! + Αναφορές μέλους + Τα μέλη μπορούν να προσθέτουν αντιδράσεις στα μηνύματα. + Τα μέλη μπορούν να διαγράψουν οριστικά τα απεσταλμένα μηνύματα. (24 ώρες) + Τα μέλη μπορούν να αναφέρουν μηνύματα στους διαχειριστές. + Τα μέλη μπορούν να στέλνουν απευθείας μηνύματα. + Τα μέλη μπορούν να στέλνουν μηνύματα που εξαφανίζονται. + Τα μέλη μπορούν να στέλνουν αρχεία και πολυμέσα. + Τα μέλη μπορούν να στέλνουν συνδέσμους SimpleX. + Τα μέλη μπορούν να στέλνουν φωνητικά μηνύματα. + Τα μέλη θα αφαιρεθούν από τη συνομιλία – αυτό δεν μπορεί να αναιρεθεί! + Τα μέλη θα αφαιρεθούν από τη ομάδα – αυτό δεν μπορεί να αναιρεθεί! + Το μέλος θα αφαιρεθεί από τη συνομιλία – αυτό δεν μπορεί να αναιρεθεί! + Το μέλος θα αφαιρεθεί από την ομάδα – αυτό δεν μπορεί να αναιρεθεί! + Το μέλος θα συμμετάσχει στην ομάδα, να γίνει αποδοχή του; + Επισήμανση μελών 👋 + Μενού & προειδοποιήσεις + μήνυμα + Μήνυμα + Σφάλμα παράδοσης μηνύματος + Αναφορές παράδοσης μηνύματος! + Προειδοποίηση παράδοσης μηνύματος + Πρόχειρο μήνυμα + Πρόχειρο μήνυμα + Το μήνυμα προωθήθηκε + Στείλε μήνυμα αμέσως μόλις πατήσεις Σύνδεση. + Το μήνυμα είναι πολύ μεγάλο! + Το μήνυμα μπορεί να παραδοθεί αργότερα όταν το μέλος γίνει ενεργό. + Πληροφορίες ουράς μηνυμάτων + Αντιδράσεις μηνυμάτων + Αντιδράσεις μηνυμάτων + Απαγορεύονται οι αντιδράσεις στα μηνύματα. + Απαγορεύονται οι αντιδράσεις στα μηνύματα σε αυτήν τη συνομιλία. + Λήψη μηνυμάτων + Εναλλακτική δρομολόγηση μηνυμάτων + Λειτουργία δρομολόγησης μηνυμάτων + Μηνύματα + ΜΗΝΥΜΑΤΑ ΚΑΙ ΑΡΧΕΙΑ + Διακομιστές μηνυμάτων + Θα εμφανιστούν τα μηνύματα από το %s! + Θα εμφανιστούν τα μηνύματα από αυτά τα μέλη! + Μορφή μηνύματος + Τα μηνύματα σε αυτήν τη συνομιλία δεν θα διαγραφούν ποτέ. + Η πηγή του μηνύματος παραμένει ιδιωτική. + Ληφθέντα μηνύματα + Απεσταλμένα μηνύματα + Κατάσταση μηνύματος + Κατάσταση μηνύματος: %s + Τα μηνύματα διαγράφηκαν αφού τα επιλέξατε. + Τα μηνύματα θα διαγραφούν - αυτό δεν μπορεί να αναιρεθεί! + Τα μηνύματα θα επισημανθούν για διαγραφή. Ο/Οι παραλήπτης/ες θα μπορούν να αποκαλύψουν αυτά τα μηνύματα. + Κείμενο μηνύματος + Το μήνυμα είναι πολύ μεγάλο + Το μήνυμα θα διαγραφεί - αυτό δεν μπορεί να αναιρεθεί! + Το μήνυμα θα επισημανθεί για διαγραφή. Ο/Οι παραλήπτης/ες θα μπορούν να αποκαλύψουν αυτό το μήνυμα. + Μικρόφωνο + Μετεγκατάσταση συσκευής + Μετεγκατάσταση από άλλη συσκευή + Μετεγκατάσταση εδώ + Μετεγκατάσταση σε άλλη συσκευή + Μετεγκατάσταση σε άλλη συσκευή μέσω QR κωδικού. + Μετεγκατάσταση σε εξέλιξη + Η μετεγκατάσταση ολοκληρώθηκε + Μετεγκαταστάσεις: %s + λεπτά + αναπάντητη κλήση + Αναπάντητη κλήση + Διαχειρίσου + διαχειρίζεται + Διαχειρίστηκε στις + Διαχειρίστηκε στις: %s + διαχειριστής + διαχειριστές + μήνες + Περισσότερα + Σύντομα έρχονται περισσότερες βελτιώσεις! + Σύντομα έρχονται περισσότερες βελτιώσεις! + Πιο αξιόπιστη σύνδεση δικτύου. + - πιο σταθερή παράδοση μηνυμάτων.\n- λίγο καλύτερες ομάδες.\n- και πολλά ακόμα! + Πιθανότατα αυτή η επαφή να έχει διαγράψει τη σύνδεση μαζί σου. + Πολλαπλά προφίλ συνομιλίας + Σίγαση + Σίγαση + Σίγαση όλων + Σε σίγαση όταν είναι ανενεργό! + Σύνδεση δικτύου + Αποκέντρωση δικτύου + Προβλήματα δικτύου - το μήνυμα έληξε μετά από πολλές προσπάθειες αποστολής. + Διαχείριση δικτύου + Χειριστής δικτύου + Χειριστές δικτύου + Δίκτυο & διακομιστές + Κατάσταση δικτύου + ποτέ + Ποτέ + Νέα συνομιλία + Νέα εμπειρία συνομιλίας 🎉 + Νέα θέματα συνομιλίας + Νέο αίτημα επαφής + Νέο αρχείο βάσης δεδομένων + Νέα εφαρμογή για υπολογιστές! + Νέο εμφανιζόμενο όνομα: + δευτερόλεπτα + Το βιογραφικό σου: + Πάτα Σύνδεση για να συνομιλήσεις + Πάτα Σύνδεση για αποστολή αιτήματος + Πάτα Σύνδεση για να χρησιμοποιήσεις το μποτ + Πατήστε Δημιουργία διεύθυνσης SimpleX στο μενού, για να τη δημιουργήσετε αργότερα. + Πάτα Συμμετοχή στην ομάδα + Πάτα για να ενεργοποιήσεις το προφίλ. + Πάτα για Σύνδεση + Πάτα για συμμετοχή + Πάτα για ανώνυμη συμμετοχή + Πάτα για επικόλληση συνδέσμου + Πάτα για σάρωση + Πάτα για να ξεκινήσεις μία νέα συνομιλία + Σύνδεση TCP + Χρόνος λήξης σύνδεσης TCP στο παρασκήνιο + Χρόνος λήξης σύνδεσης TCP + Θύρα TCP για ανταλλαγή μηνυμάτων + Σφάλμα προσωρινού αρχείου + Η δοκιμή απέτυχε στο βήμα %s. + Δοκιμή διακομιστή + Δοκιμή διακομιστών + Ευχαριστούμε τους χρήστες – συνεισφέρετε μέσω του Weblate! + Ευχαριστούμε τους χρήστες – συνεισφέρετε μέσω του Weblate! + Ευχαριστούμε τους χρήστες – συνεισφέρετε μέσω του Weblate! + Ευχαριστούμε τους χρήστες – συνεισφέρετε μέσω του Weblate! + Ευχαριστούμε τους χρήστες – συνεισφέρετε μέσω του Weblate! + Σε ευχαριστούμε που εγκατέστησες το SimpleX Chat! + Η διεύθυνση θα είναι σύντομη και το προφίλ σου θα κοινοποιηθεί μέσω αυτής. + Η εφαρμογή λαμβάνει νέα μηνύματα περιοδικά — καταναλώνει ένα μικρό ποσοστό της μπαταρίας ανά ημέρα. Η εφαρμογή δεν χρησιμοποιεί ειδοποιήσεις push — τα δεδομένα από τη συσκευή σου δεν αποστέλλονται στους διακομιστές. + Η εφαρμογή ενδέχεται να κλείσει μετά από 1 λεπτό στο παρασκήνιο. + Η εφαρμογή προστατεύει το απόρρητό σου χρησιμοποιώντας διαφορετικούς χειριστές σε κάθε συνομιλία. + Η εφαρμογή θα ζητήσει επιβεβαίωση για λήψεις από άγνωστους διακομιστές αρχείων (εκτός από .onion ή όταν είναι ενεργοποιημένος ο διακομιστής μεσολάβησης SOCKS). + Η προσπάθεια αλλαγής της φράσης πρόσβασης της βάσης δεδομένων δεν ολοκληρώθηκε. + Ο κωδικός που σάρωσες δεν είναι κωδικός QR ενός συνδέσμου SimpleX. + Η σύνδεση έφτασε στο όριο των μη παραδοθέντων μηνυμάτων, η επαφή σου ενδέχεται να είναι εκτός σύνδεσης. + Η σύνδεση που αποδέχθηκες θα ακυρωθεί! + Η επαφή με την οποία μοιράστηκες αυτόν το σύνδεσμο, ΔΕΝ θα μπορεί να συνδεθεί! + Η βάση δεδομένων δεν λειτουργεί σωστά. Πάτησε για να μάθεις περισσότερα. + Για τις κλήσεις απαιτείται ο προεπιλεγμένος περιηγητής. Ρύθμισε τον προεπιλεγμένο περιηγητή στο σύστημα σου και μοιράσου περισσότερες πληροφορίες με τους προγραμματιστές. + Το όνομα της συσκευής θα κοινοποιηθεί στην εφαρμογή του συνδεδεμένου κινητού. + Η κρυπτογράφηση λειτουργεί και η νέα κρυπτογράφηση δεν είναι απαραίτητη. Μπορεί να προκαλέσει σφάλματα σύνδεσης! + Το μέλλον στην ανταλλαγή μηνυμάτων + Ο κωδικός ελέγχου του προηγούμενου μηνύματος είναι διαφορετικός. + Ο αναγνωριστικός κωδικός του επόμενου μηνύματος είναι λανθασμένος (μικρότερος ή ίσος με τον προηγούμενο).\nΑυτό μπορεί να συμβεί λόγω κάποιου σφάλματος ή όταν η σύνδεση έχει παραβιαστεί. + Η εικόνα δεν μπορεί να αποκωδικοποιηθεί. Δοκίμασε μια άλλη εικόνα ή επικοινώνησε με τους προγραμματιστές. + Ο σύνδεσμος θα είναι σύντομος και το προφίλ της ομάδας θα κοινοποιηθεί μέσω αυτού. + Θέμα + ΘΕΜΑΤΑ + Τα μηνύματα θα διαγραφούν για όλα τα μέλη. + Τα μηνύματα θα επισημαίνονται ως ελεγχόμενα για όλα τα μέλη. + Το μήνυμα θα διαγραφεί για όλα τα μέλη. + Το μήνυμα θα επισημανθεί ως υπό έλεγχο για όλα τα μέλη. + Η πλατφόρμα μηνυμάτων και εφαρμογών που προστατεύει το απόρρητο και την ασφάλειά σου. + Η φράση πρόσβασης αποθηκεύεται στις ρυθμίσεις ως απλό κείμενο. + Η φράση πρόσβασης θα αποθηκευτεί στις ρυθμίσεις ως απλό κείμενο μετά την αλλαγή της ή την επανεκκίνηση της εφαρμογής. + Το προφίλ κοινοποιείται μόνο στις επαφές σου. + Η αναφορά θα αρχειοθετηθεί για εσένα. + Ο ρόλος θα αλλάξει σε %s. Όλοι οι συμμετέχοντες στη συνομιλία θα ειδοποιηθούν. + Ο ρόλος θα αλλάξει σε %s. Όλα τα μέλη της ομάδας θα ενημερωθούν. + Ο ρόλος θα αλλάξει σε %s. Το μέλος θα λάβει νέα πρόσκληση. + Ο δεύτερος προκαθορισμένος χειριστής στην εφαρμογή! + Το δεύτερο τικ που χάσαμε! ✅ + Ο αποστολέας ΔΕΝ θα ειδοποιηθεί. + Οι διακομιστές για τις νέες συνδέσεις του τρέχοντος προφίλ συνομιλίας σου + Οι διακομιστές για τα νέα αρχεία του τρέχοντος προφίλ συνομιλίας σου + Αυτές οι ρυθμίσεις ισχύουν για το τρέχον προφίλ σου + Το κείμενο που επικόλλησες δεν είναι σύνδεσμος SimpleX. + Το αρχείο της βάσης δεδομένων που μεταφορτώθηκε, θα διαγραφεί οριστικά από τους διακομιστές. + Το βίντεο δεν μπορεί να αποκωδικοποιηθεί. Δοκίμασε ένα άλλο βίντεο ή επικοινώνησε με τους προγραμματιστές. + Μπορούν να παρακαμφθούν στις ρυθμίσεις επαφών και ομάδων. + Αυτή η ενέργεια δεν μπορεί να αναιρεθεί - όλα τα ληφθέντα και απεσταλμένα αρχεία και πολυμέσα θα διαγραφούν. Οι εικόνες χαμηλής ανάλυσης θα παραμείνουν. + Αυτή η ενέργεια δεν μπορεί να αναιρεθεί - τα μηνύματα που έχουν αποσταλεί και παραληφθεί πριν από την επιλεγμένη ημερομηνία, θα διαγραφούν. Η διαδικασία μπορεί να διαρκέσει αρκετά λεπτά. + Αυτή η ενέργεια δεν μπορεί να αναιρεθεί - τα μηνύματα που έχουν αποσταλεί και παραληφθεί σε αυτήν τη συνομιλία, πριν από την επιλεγμένη ημερομηνία, θα διαγραφούν. + Αυτή η ενέργεια δεν μπορεί να αναιρεθεί - το προφίλ, οι επαφές, τα μηνύματα και τα αρχεία σου, θα χαθούν οριστικά και ανεπανόρθωτα. + Αυτή η συνομιλία προστατεύεται με κρυπτογράφηση από άκρη-σε-άκρη. + Αυτή η συνομιλία προστατεύεται με κβαντο-ανθεκτική κρυπτογράφηση από άκρη-σε-άκρη. + Αυτή η συσκευή + Το όνομα αυτής της συσκευής + Το εμφανιζόμενο όνομα δεν είναι έγκυρο. Επέλεξε ένα άλλο όνομα. + Αυτή η λειτουργία δεν υποστηρίζεται ακόμη. Δικίμασε την επόμενη έκδοση. + Αυτή η ομάδα έχει πάνω από %1$d μέλη, δεν αποστέλλονται αναφορές παράδοσης. + Αυτή η ομάδα δεν υπάρχει πλέον. + Αυτός είναι ο δικός σου σύνδεσμος 1-χρήσης! + Αυτή είναι η διεύθυνση σου SimpeX! + Αυτός ο σύνδεσμος δεν είναι έγκυρος! + Αυτός ο σύνδεσμος απαιτεί νεότερη έκδοση της εφαρμογής. Αναβάθμισε την εφαρμογή ή ζήτησε από την επαφή σου να σου στείλει ένα συμβατό σύνδεσμο. + Αυτός ο σύνδεσμος χρησιμοποιήθηκε με άλλη κινητή συσκευή. Δημιούργησε ένα νέο σύνδεσμο στον υπολογιστή σου. + Αυτό το μήνυμα διαγράφηκε ή δεν έχει ληφθεί ακόμα. + Αυτός ο κωδικός QR δεν είναι σύνδεσμος! + Αυτή η ρύθμιση ισχύει για τα μηνύματα στο τρέχον προφίλ συνομιλίας σου. + Αυτή η ρύθμιση αφορά το τρέχον προφίλ σου. + Αυτό το κείμενο δεν είναι σύνδεσμος! + Αυτό το κείμενο είναι διαθέσιμο στις ρυθμίσεις + Εξαντλήθηκε ο χρόνος αναμονής κατά τη σύνδεση με τον υπολογιστή + Ο χρόνος εξαφάνισης ορίζεται μόνο για τις νέες επαφές. + Τίτλος + Για να επιτρέψεις σε μια εφαρμογή κινητού να συνδεθεί στον υπολογιστή, άνοιξε αυτήν τη θύρα στο τείχος προστασίας σου, εάν το έχεις ενεργοποιήσει. + Για να λαμβάνεις ειδοποιήσεις σχετικά με τις νέες εκδόσεις, ενεργοποίησε τον περιοδικό έλεγχο για σταθερές ή δοκιμαστικές εκδόσεις. + Για να συνδεθείς μέσω συνδέσμου + Για να συνδεθείς, η επαφή σου μπορεί να σαρώσει τον κωδικό QR ή να χρησιμοποιήσει τον σύνδεσμο στην εφαρμογή. + Εναλλαγή λίστας συνομιλιών: + Ενεργοποίηση ανώνυμης λειτουργίας κατά τη σύνδεση. + Για απόκρυψη ανεπιθύμητων μηνυμάτων. + Για να πραγματοποιήσεις κλήσεις, επέτρεψε τη χρήση του μικροφώνου σου. Τερμάτισε την κλήση και προσπάθησε να καλέσεις ξανά. + Πάρα πολλές εικόνες! + Πάρα πολλά βίντεο! + Για να προστατευτείς από αντικατάσταση του συνδέσμου σου, μπορείς να συγκρίνεις τους κωδικούς ασφαλείας των επαφών σου. + Για την προστασία της ζώνης ώρας, τα αρχεία εικόνας/φωνής χρησιμοποιούν UTC ώρα. + Για να προστατεύσεις τις πληροφορίες σου, ενεργοποίησε το SimpleX Lock.\nΘα σου ζητηθεί να ολοκληρώσεις την επαλήθευση ταυτότητας πριν ενεργοποιηθεί αυτή η λειτουργία. + Για την προστασία της IP διεύθυνσής σου, η ιδιωτική δρομολόγηση χρησιμοποιεί τους διακομιστές SMP για την παράδοση μηνυμάτων. + Για την προστασία της ιδιωτικότητάς σου, το SimpleX χρησιμοποιεί ξεχωριστά αναγνωριστικά για κάθε μία από τις επαφές σου. + Για λήψη + Για να λαμβάνεις ειδοποιήσεις, παρακαλώ εισήγαγε τη φράση πρόσβασης της βάσης δεδομένων. + Για να αποκαλύψεις το κρυφό προφίλ σου, εισήγαγε έναν πλήρη κωδικό στο πεδίο αναζήτησης στη σελίδα Τα προφίλ συνομιλίας σου. + Για αποστολή + Για αποστολή εντολών, θα πρέπει να είσαι συνδεδεμένος. + (για διαμοιρασμό με την επαφή σου) + Για να ξεκινήσεις μία νέα συνομιλία + Συνολικά + Για να χρησιμοποιήσεις άλλο προφίλ μετά την προσπάθεια σύνδεσης, διέγραψε τη συνομιλία και χρησιμοποίησε ξανά τον σύνδεσμο. + Για να επαληθεύσεις την κρυπτογράφηση από άκρη-σε-άκρη με την επαφή σου, συγκρίνετε (ή σαρώστε) τον κωδικό στις συσκευές σας. + Διαφάνεια + Απομόνωση μεταφοράς + Απομόνωση μεταφοράς + Μεταφορές συνεδριών + Ενεργοποίηση + μη εξουσιοδοτημένη αποστολή + Ξεμπλοκάρισμα + ξεμπλοκαρισμένο %s + Ξεμπλοκάρισμα για όλους + Ξεμπλοκάρισμα μέλους + Ξεμπλοκάρισμα μέλους; + Ξεμπλοκάρισμα μέλους για όλους; + Ξεμπλοκάρισμα μελών για όλους; + Μηνύματα που δεν παραδόθηκαν + Αφαίρεση από τα αγαπημένα + Εμφάνιση + Εμφάνιση προφίλ συνομιλίας + Εμφάνιση προφίλ + άγνωστο + Άγνωστο σφάλμα βάσης δεδομένων: %s + Άγνωστο σφάλμα + άγνωστη μορφή μηνύματος + Άγνωστοι διακομιστές + Άγνωστοι διακομιστές! + άγνωστη κατάσταση + Εκτός αν η επαφή σου διέγραψε τη σύνδεση ή αυτός ο σύνδεσμος είχε ήδη χρησιμοποιηθεί, μπορεί να πρόκειται για σφάλμα - παρακαλούμε να το αναφέρεις.\nΓια να συνδεθείς, ζήτησε από την επαφή σου να δημιουργήσει έναν άλλο σύνδεσμο σύνδεσης και έλεγξε ότι έχεις σταθερή σύνδεση δικτύου. + Αποσύνδεση + Αποσύνδεση υπολογιστή; + Ξεκλείδωμα + Απενεργοποίηση σίγασης + Απενεργοποίηση σίγασης + Απροστάτευτο + αδιάβαστο + Μη αναγνωσμένες αναφορές + Μη υποστηριζόμενος σύνδεσμος σύνδεσης + Αναβάθμιση + Αναβάθμιση + Αναβάθμιση + Διαθέσιμη αναβάθμιση: %s + Ενημέρωση φράσης πρόσβασης της βάσης δεδομένων + Ενημερωμένοι όροι + ενημερωμένο προφίλ ομάδας + Η λήψη της ενημέρωσης ακυρώθηκε + ενημερωμένο προφίλ + Ενημέρωση ρυθμίσεων δικτύου; + Ενημέρωση της λειτουργίας απομόνωσης μεταφοράς; + Ενημέρωσε τη διεύθυνσή σου + Η ενημέρωση των ρυθμίσεων θα επανασυνδέσει την εφαρμογή με όλους τους διακομιστές. + Αναβάθμιση + Αναβάθμιση διεύθυνσης + Αναβάθμιση διεύθυνσης; + Αναβάθμιση και άνοιγμα συνομιλίας + Αυτόματη αναβάθμιση εφαρμογής + Αναβάθμιση συνδέσμου ομάδας + Αναβάθμιση συνδέσμου ομάδας; + Ανέβηκε + Ανεβασμένα αρχεία + Σφάλματα μεταφόρτωσης + Αποτυχία μεταφόρτωσης + Ανέβασμα αρχείου + Ανεβαίνει το αρχείο αρχειοθέτησης + Τα τελευταία 100 μηνύματα αποστέλλονται στα νέα μέλη. + Χρήση συνομιλίας + Χρησιμοποίησε διαφορετικά διαπιστευτήρια διακομιστή μεσολάβησης για κάθε σύνδεση. + Χρησιμοποίησε διαφορετικά διαπιστευτήρια διακομιστή μεσολάβησης για κάθε προφίλ. + Χρήση απευθείας σύνδεσης στο Διαδίκτυο; + Χρήση για αρχεία + Χρήση για μηνύματα + Χρήση για νέες συνδέσεις + Χρήση από τον υπολογιστή + Χρήση ανώνυμου προφίλ + Χρήση κεωτρικών διακομιστών .onion + Χρήση ιδιωτικής δρομολόγησης με άγνωστους διακομιστές. + Χρήση ιδιωτικής δρομολόγησης με άγνωστους διακομιστές όταν η διεύθυνση IP δεν προστατεύεται. + Χρήση τυχαίων διαπιστευτηρίων + Χρήση τυχαίας φράσης πρόσβασης + Όνομα χρήστη + Χρήση %s + Χρήση διακομιστή + Χρήση διακομιστών + Χρήση διακομιστών SimpleX Chat; + Χρήση δικομιστή μεσολάβησης SOCKS + Χρήση διακομιστή μεσολάβησης SOCKS; + Χρήση της θύρας TCP %1$s όταν δεν έχει καθοριστεί θύρα. + Χρήση της θύρας TCP 443 μόνο για προκαθορισμένους διακομιστές. + Χρήση της εφαρμογής κατά τη διάρκεια μίας κλήσης. + Χρήση της εφαρμογής με το ένα χέρι + Χρήση θύρας web + Χρήση διακομιστών SimpleX Chat. + Σφάλμα κατά την προσθήκη μέλους/ων + Σφάλμα κατά την προσθήκη διακομιστή + Σφάλμα κατά το μπλοκάρισμα του μέλους, για όλους + Σφάλμα κατά την αλλαγή διεύθυνσης + Σφάλμα κατά την αλλαγή προφίλ + Σφάλμα κατά την αλλαγή ρόλου + Σφάλμα κατά την αλλαγή της ρύθμισης + Σφάλμα κατά τη σύνδεση με το διακομιστή προώθησης %1$s. Παρακαλώ δοκίμασε ξανά αργότερα. + Σφάλμα κατά τη σύνδεση με τον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτή τη σύνδεση: %1$s. + Σφάλμα κατά τη δημιουργία διεύθυνσης + Σφάλμα κατά τη δημιουργία της λίστας συνομιλιών + Σφάλμα κατά τη δημιουργία συνδέσμου ομάδας + Σφάλμα κατά τη δημιουργία επαφής μέλους + Σφάλμα κατά τη δημιουργία μηνύματος + Σφάλμα κατά τη δημιουργία προφίλ! + Σφάλμα κατά τη δημιουργία της αναφοράς + Σφάλμα κατά τη διαγραφή της συνομιλίας + Σφάλμα κατά τη διαγραφή της βάσης δεδομένων συνομιλιών + Σφάλμα κατά τη διαγραφή της επαφής + Σφάλμα κατά τη διαγραφή του αιτήματος της επαφής + Σφάλμα κατά τη διαγραφή της βάσης δεδομένων + Σφάλμα κατά τη διαγραφή ομάδας + Σφάλμα κατά τη διαγραφή του συνδέσμου ομάδας + Σφάλμα κατά τη διαγραφή εκκρεμούς σύνδεσης επαφής + Σφάλμα κατά τη διαγραφή ιδιωτικών σημειώσεων + Σφάλμα κατά τη διαγραφή του προφίλ χρήστη + Σφάλμα κατά τη λήψη του αρχείου αρχειοθέτησης + Σφάλμα κατά την ενεργοποίηση των αναφορών παράδοσης! + Σφάλμα κατά την κρυπτογράφηση της βάσης δεδομένων + Σφάλμα κατά την εξαγωγή της βάσης δεδομένων συνομιλιών + Σφάλμα κατά την εξαγωγή της βάσης δεδομένων συνομιλιών + Σφάλμα προώθησης μηνυμάτων + Σφάλμα κατά την εισαγωγή της βάσης δεδομένων συνομιλιών + Σφάλμα κατά την αρχικοποίηση του WebView. Βεβαιώσου ότι έχεις εγκαταστήσει το WebView και ότι η υποστηριζόμενη αρχιτεκτονική είναι arm64.\nΣφάλμα: %s + Σφάλμα κατά την αρχικοποίηση του WebView. Ενημέρωσε το σύστημά σου στη νέα έκδοση. Επικοινώνησε με τους προγραμματιστές.\nΣφάλμα: %s + Σφάλμα κατά τη συμμετοχή στην ομάδα + Σφάλμα κατά τη φόρτωση των λιστών συνομιλιών + Σφάλμα κατά τη φόρτωση των λεπτομερειών + Σφάλμα κατά τη φόρτωση των διακομιστών SMP + Σφάλμα κατά τη φόρτωση των διακομιστών XFTP + Σφάλμα επισήμανσης ως αναγνωσμένου + Σφάλμα κατά το άνοιγμα του προγράμματος περιήγησης + Σφάλμα κατά το άνοιγμα της συνομιλίας + Σφάλμα κατά το άνοιγμα της ομάδας + Σφάλμα κατά την ανάγνωση της φράσης πρόσβασης της βάσης δεδομένων + Σφάλμα κατά τη λήψη του αρχείου + Σφάλμα κατά την επανασύνδεση του διακομιστή + Σφάλμα κατά την επανασύνδεση των διακομιστών + Σφάλμα κατά την απόρριψη αιτήματος της επαφής + Σφάλμα κατά την αφαίρεση του μέλους + Σφάλμα επαναφοράς στατιστικών στοιχείων + Σφάλμα: %s + Σφάλματα + Σφάλμα κατά την αποθήκευση της βάσης δεδομένων + Σφάλμα κατά την αποθήκευση του αρχείου + Σφάλμα κατά την αποθήκευση του προφίλ ομάδας + Σφάλμα κατά την αποθήκευση των διακομιστών ICE + Σφάλμα κατά την αποθήκευση του διακομιστή μεσολάβησης + Σφάλμα κατά την αποθήκευση διακομιστών + Σφάλμα κατά την αποθήκευση των ρυθμίσεων + Σφάλμα κατά την αποθήκευση των ρυθμίσεων + Σφάλμα κατά την αποθήκευση των διακομιστών SMP + Σφάλμα κατά την αποθήκευση του κωδικού πρόσβασης χρήστη + Σφάλμα κατά την αποθήκευση διακομιστών XFTP + Σφάλμα κατά την αποστολή της πρόσκλησης + Σφάλμα κατά την αποστολή του μηνύματος + Σφάλμα κατά τη ρύθμιση της διεύθυνσης + σφάλμα κατά την εμφάνιση του περιεχομένου + σφάλμα εμφάνισης μηνύματος + Σφάλμα στην εμφάνιση της ειδοποίησης, επικοινώνησε με τους προγραμματιστές. + Σφάλματα στη διαμόρφωση των διακομιστών. + Σφάλμα κατά την έναρξη της συνομιλίας + Σφάλμα κατά τη διακοπή της συνομιλίας + Σφάλμα κατά την εναλλαγή προφίλ + Σφάλμα κατά την αλλαγή προφίλ! + Σφάλμα κατά τo συγχρονισμό της σύνδεσης + Σφάλμα κατά την ενημέρωση της λίστας συνομιλιών + Σφάλμα κατά την ενημέρωση του συνδέσμου ομάδας + Σφάλμα κατά την ενημέρωση της διαμόρφωσης δικτύου + Σφάλμα κατά την αναβάθμιση του διακομιστή + Σφάλμα κατά την ενημέρωση των ρυθμίσεων απορρήτου χρήστη + Σφάλμα κατά το ανέβασμα του αρχείου αρχειοθέτησης + Σφάλμα κατά την επαλήθευση της φράσης πρόσβασης: + Ακόμα και όταν είναι απενεργοποιημένη στη συνομιλία. + Η εκτέλεση της λειτουργίας διαρκεί πολύ χρόνο: %1$d δευτερόλεπτα: %2$s + Έξοδος χωρίς αποθήκευση + Επέκτεινε + Επέκταση επιλογής ρόλου + ΠΕΙΡΑΜΑΤΙΚΟ + Πειραματικά χαρακτηριστικά + έληξε + Εξαγωγή της βάσης δεδομένων + Το εξαγόμενο αρχείο δεν υπάρχει + Εξαγωγή θέματος + Αποτυχία φόρτωσης συνομιλίας + Αποτυχία φόρτωσης συνομιλιών + Γρήγορα και χωρίς αναμονή μέχρι να συνδεθεί ο αποστολέας! + Ταχύτερη διαγραφή ομάδων. + Ταχύτερη σύνδεση και πιο αξιόπιστα μηνύματα. + Ταχύτερη αποστολή μηνυμάτων. + Αγαπημένο + Αγαπημένα + Αρχείο + Αρχείο + Σφάλμα αρχείου + Το αρχείο έχει αποκλειστεί από το χειριστή του διακομιστή:\n%1$s. + Το αρχείο δεν βρέθηκε + Το αρχείο δεν βρέθηκε - πιθανότατα το αρχείο διαγράφηκε ή ακυρώθηκε. + Αρχείο: %s + Αρχεία + ΑΡΧΕΙΑ + Αρχεία και πολυμέσα + Απαγορεύονται τα αρχεία και τα πολυμέσα. + Τα αρχεία και τα πολυμέσα, απαγορεύονται σε αυτήν τη συνομιλία. + Δεν επιτρέπονται αρχεία και πολυμέσα + Απαγορεύονται αρχεία και πολυμέσα! + Το αρχείο αποθηκεύτηκε + Σφάλμα διακομιστή αρχείων: %1$s + Αρχεία & πολυμέσα + Κατάσταση αρχείου + Κατάσταση αρχείου: %s + Το αρχείο διαγράφηκε ή ο σύνδεσμος δεν είναι έγκυρος. + Το αρχείο θα διαγραφεί από τους διακομιστές. + Το αρχείο θα ληφθεί όταν η επαφή σου ολοκληρώσει τη μεταφόρτωσή του. + Το αρχείο θα ληφθεί όταν η επαφή σου είναι συνδεδεμένη, παρακαλώ περίμενε ή έλεγξε αργότερα! + Γέμισμα οθόνης + Φίλτραρε τις μη αναγνωσμένες και τις αγαπημένες συνομιλίες. + Ολοκλήρωση της μετεγκατάστασης + Ολοκλήρωσε τη μετεγκατάσταση σε άλλη συσκευή. + Επιτέλους, τα έχουμε! 🚀 + Βρες τις συνομιλίες πιο γρήγορα + Βρες αυτήν την άδεια στις ρυθμίσεις Android και παραχώρησέ την χειροκίνητα. + Το αποτύπωμα στη διεύθυνση του διακομιστή προορισμού δεν ταιριάζει με το πιστοποιητικό: %1$s. + Το αποτύπωμα στη διεύθυνση του διακομιστή προώθησης δεν ταιριάζει με το πιστοποιητικό: %1$s. + Το αποτύπωμα στη διεύθυνση του διακομιστή δεν ταιριάζει με το πιστοποιητικό. + Το αποτύπωμα στη διεύθυνση του διακομιστή δεν ταιριάζει με το πιστοποιητικό: %1$s. + Προσαρμογή στην οθόνη + Επιδιόρθωση + Επιδιόρθωση + Επιδιόρθωση σύνδεσης + Νέος ρόλος ομάδας: Συντονιστής + Νέο στο %s + Επιλογές νέων πολυμέσων + Νέος ρόλος μέλους + Νέο μέλος θέλει να ενταχθεί στην ομάδα. + νέο μήνυμα + Νέο μήνυμα + Νέα συσκευή τηλεφώνου + Νέος κωδικός πρόσβασης + Νέα φράση πρόσβασης + Νέος διακομιστής + Κάθε φορά που εκκινείς την εφαρμογή, θα χρησιμοποιούνται νέα διαπιστευτήρια SOCKS. + Νέα διαπιστευτήρια SOCKS θα χρησιμοποιούνται για κάθε διακομιστή. + όχι + Όχι + Όχι + Όχι + Χωρίς κωδικό πρόσβασης εφαρμογής + Χωρίς κλήσεις στο παρασκήνιο + Χωρίς υπηρεσία παρασκηνίου + Χωρίς συνομιλίες + Δεν βρέθηκαν συνομιλίες + Δεν υπάρχουν συνομιλίες στη λίστα %s. + Δεν υπάρχουν συνομιλίες με μέλη + Δεν υπάρχει συνδεδεμένο κινητό + Δεν έχουν επιλεγεί επαφές + Δεν υπάρχουν επαφές για προσθήκη + Δεν υπάρχουν πληροφορίες παράδοσης + χωρίς λεπτομέρειες + Δεν υπάρχει ακόμη άμεση σύνδεση, το μήνυμα προωθείται από το διαχειριστή. + χωρίς κρυπτογράφηση e2e + Καμία φιλτραρισμένη συνομιλία + Καμία φιλτραρισμένη επαφή + Χωρίς ιστορικό + Δεν υπάρχουν πληροφορίες, δοκίμασε να επαναφορτώσεις + Χωρίς διακομιστές πολυμέσων και αρχείων. + Κανένα μήνυμα + Χωρίς διακομιστές μηνυμάτων. + κανένα + Δεν υπάρχει σύνδεση δικτύου + Καμία συνεδρία ιδιωτικής δρομολόγησης + Δεν υπάρχουν ληφθέντα ή απεσταλμένα αρχεία + Δεν έχει επιλεγεί συνομιλία + Δεν υπάρχουν διακομιστές για τη δρομολόγηση ιδιωτικών μηνυμάτων. + Δεν υπάρχουν διακομιστές για τη λήψη αρχείων. + Δεν υπάρχουν διακομιστές για τη λήψη μηνυμάτων. + Δεν υπάρχουν διακομιστές για την αποστολή αρχείων. + χωρίς συνδρομή + Μη συμβατό! + Σημειώσεις + χωρίς κείμενο + Δεν έχει επιλεγεί τίποτα + Δεν υπάρχει τίποτα να προωθήσεις! + Προεπισκόπηση ειδοποίησης + Ειδοποιήσεις + Ειδοποιήσεις και μπαταρία + Υπηρεσία ειδοποιήσεων + Οι ειδοποιήσεις θα παραδίδονται μόνο μέχρι να σταματήσει η εφαρμογή! + Οι ειδοποιήσεις θα σταματήσουν να λειτουργούν μέχρι να επανεκκινήσεις την εφαρμογή. + μη συγχρονισμένο + Δεν υπάρχουν μη αναγνωσμένες συνομιλίες + Χωρίς αναγνωριστικά χρήστη. + Τώρα οι διαχειριστές μπορούν:\n- να διαγράφουν τα μηνύματα των μελών.\n- να απενεργοποιούν μέλη (ρόλος παρατηρητή) + παρατηρητής + κλειστό` + κλειστό + κλειστό + Κλειστό + Κλειστή + προσφέρεται %s + προσφέρθηκε %s: %2s + ΟΚ + Παλιό αρχείο βάσης δεδομένων + ανοιχτό + Σύνδεσμος πρόσκλησης 1-χρήσης + Σύνδεσμος πρόσκλησης 1-χρήσης + Για τη σύνδεση θα απαιτηθούν διακομιστές Onion.\nΣημείωση: δεν θα μπορείς να συνδεθείς στους διακομιστές χωρίς διεύθυνση .onion. + Οι κεντρικοί υπολογιστές Onion θα χρησιμοποιούνται όταν είναι διαθέσιμοι. + Οι κεντρικοί υπολογιστές Onion δεν θα χρησιμοποιηθούν. + Μπορούν να σταλούν μόνο 10 εικόνες ταυτόχρονα + Μπορούν να σταλούν μόνο 10 βίντεο ταυτόχρονα + Μόνο οι ιδιοκτήτες του chat μπορούν να αλλάξουν τις προτιμήσεις. + Μόνο οι συσκευές αποθηκεύουν προφίλ χρηστών, επαφές, ομάδες και μηνύματα. + Διαγραφή μόνο της συνομιλίας + Μόνο οι ιδιοκτήτες ομάδων μπορούν να αλλάξουν τις προτιμήσεις της ομάδας. + Μόνο οι ιδιοκτήτες ομάδων μπορούν να ενεργοποιήσουν αρχεία και πολυμέσα. + Μόνο οι ιδιοκτήτες ομάδων μπορούν να ενεργοποιήσουν τα φωνητικά μηνύματα. + Μόνο μία συσκευή μπορεί να λειτουργεί ταυτόχρονα + Μόνο ο αποστολέας και οι διαχειριστές μπορούν να το δουν + (αποθηκεύεται μόνο από τα μέλη της ομάδας) + Μόνο εσύ και οι διαχειριστές το βλέπετε + Μόνο εσύ μπορείς να προσθέσεις αντιδράσεις σε μηνύματα. + Μόνο εσύ μπορείς να διαγράψεις οριστικά τα μηνύματα (η επαφή σου μπορεί να τα επισημάνει για διαγραφή). (24 ώρες) + Μόνο εσύ μπορείς να πραγματοποιήσεις κλήσεις. + Μόνο εσύ μπορείς να στέλνεις μηνύματα που εξαφανίζονται. + Μόνο εσύ μπορείς να στέλνεις αρχεία και πολυμέσα. + Μόνο εσύ μπορείς να στέλνεις φωνητικά μηνύματα. + Μόνο η επαφή σου μπορεί να προσθέσει αντιδράσεις σε μηνύματα. + Μόνο η επαφή σου μπορεί να διαγράψει οριστικά τα μηνύματα (μπορείς να τα επισημάνεις για διαγραφή). (24 ώρες) + Μόνο η επαφή σου μπορεί να πραγματοποιεί κλήσεις. + Μόνο η επαφή σου μπορεί να στείλει μηνύματα που εξαφανίζονται. + Μόνο η επαφή σου μπορεί να στείλει αρχεία και πολυμέσα. + Μόνο η επαφή σου μπορεί να στείλει φωνητικά μηνύματα. + άνοιγμα + Άνοιξε + Άνοιγμα + Άνοιξε τις ρυθμίσεις της εφαρμογής + Ανοιχτές αλλαγές + Άνοιγμα συνομιλίας + Άνοιγμα συνομιλίας + Άνοιγμα κονσόλας συνομιλίας + - Άνοιγμα συνομιλίας στο πρώτο μη αναγνωσμένο μήνυμα.\n- Μετάβαση στα αναφερόμενα μηνύματα. + Άνοιγμα καθαρού συνδέσμου + Ανοιχτές προϋποθέσεις + Άνοιγμα φακέλου βάσης δεδομένων + Άνοιγμα θέσης αρχείου + Άνοιγμα πλήρους συνδέσμου + Άνοιγμα ομάδας + Το άνοιγμα του συνδέσμου στον περιηγητή μπορεί να μειώσει την ιδιωτικότητα και την ασφάλεια της σύνδεσης. Οι μη αξιόπιστοι σύνδεσμοι SimpleX θα εμφανίζονται με κόκκινο χρώμα. + Άνοιγμα συνδέσμου + Άνοιγμα συνδέσμων από τη λίστα συνομιλιών + Άνοιξε την οθόνη μετεγκατάστασης + Άνοιξε νέα συνομιλία + Άνοιξε νέα ομάδα + Άνοιγμα θύρας στο τείχος προστασίας + Άνοιξε τις Ρυθμίσεις Safari / Ιστοσελίδες / Μικρόφωνο και στη συνέχεια επέλεξε Να επιτρέπεται για το localhost. + Άνοιγμα ρυθμίσεων διακομιστή + Άνοιγμα ρυθμίσεων + Άνοιξε το SimpleX Chat για να αποδεχθείς την κλήση + Άνοιξε για να αποδεχθείς + Άνοιξε για να συνδεθείς + Άνοιξε για να συμμετάσχεις + Άνοιξε για να χρησιμοποιήσεις το μποτ + Άνοιγμα συνδέσμου ιστού; + Άνοιγμα με %s + Χειριστής + Διακομιστής χειριστή + - προαιρετική ειδοποίηση για διεγραμμένες επαφές.\n- ονόματα προφίλ με κενά.\n- και πολλά άλλα! + Οργάνωσε τις συνομιλίες σε λίστες + Ή εισαγωγή αρχείου αρχειοθέτησης + Ή επικόλλησε το σύνδεσμο του αρχείου αρχειοθέτησης + Ή σάρωσε τον κωδικό QR + Ή μοιράσου με ασφάλεια αυτόν τον σύνδεσμο αρχείου + Ή δείξε αυτόν τον κωδικό + Ή για να μοιραστείς ιδιωτικά + άλλο + Άλλο + άλλα σφάλματα + Άλλοι διακομιστές SMP + Άλλοι διακομιστές XFTP + ιδιοκτήτης + ιδιοκτήτες + Κωδικός πρόσβασης + Ο κωδικός πρόσβασης αλλάχθηκε! + Εισαγωγή κωδικού πρόσβασης + Ο κωδικός πρόσβασης δεν έχει αλλάξει! + Ο κωδικός πρόσβασης έχει οριστεί! + Η φράση πρόσβασης στο Keystore δεν μπορεί να διαβαστεί, παρακαλώ εισήγαγέ τη χειροκίνητα. Αυτό μπορεί να συνέβη μετά από ενημέρωση του συστήματος που δεν είναι συμβατή με την εφαρμογή. Εάν δεν είναι αυτή η περίπτωση, παρακαλώ επικοινώνησε με τους προγραμματιστές. + Η φράση πρόσβασης στο Keystore δεν μπορεί να διαβαστεί. Αυτό μπορεί να συνέβη μετά από ενημέρωση του συστήματος που δεν είναι συμβατή με την εφαρμογή. Εάν δεν είναι αυτή η περίπτωση, επικοινώνησε με τους προγραμματιστές. + Απαιτείται φράση πρόσβασης + Ο φράση πρόσβασης δεν βρέθηκε στο Keystore, παρακαλώ εισήγαγέ τη χειροκίνητα. Αυτό μπορεί να συνέβη αν επανέφερες τα δεδομένα της εφαρμογής χρησιμοποιώντας ένα εργαλείο δημιουργίας αντιγράφων ασφαλείας. Αν δεν είναι αυτή η περίπτωση, παρακαλώ επικοινώνησε με τους προγραμματιστές. + Κωδικός + Κωδικός για εμφάνιση + Επικόλληση + Επικόλληση συνδέσμου αρχείου αρχειοθέτησης + Επικόλληση διεύθυνσης υπολογιστή + Επικόλληση συνδέσμου + Επικόλλησε το σύνδεσμο για να συνδεθείς! + Επικόλλησε το σύνδεσμο που έλαβες + Επικόλλησε το σύνδεσμο που έλαβες για να συνδεθείς με την επαφή σου… + από άκρη-σε-άκρη + εκκρεμής + Εκκρεμής + Εκκρεμής + σε αναμονή έγκρισης + Εκκρεμής κλήση + σε αναμονή για έλεγχο + Περιοδικά + Περιοδικές ειδοποιήσεις + Οι περιοδικές ειδοποιήσεις είναι απενεργοποιημένες! + Η άδεια απορρίφθηκε! + Διεπαφή στα Περσικά + Κλήσεις σε λειτουργία εικόνα-μέσα-στην-εικόνα + Μέτρηση PING + εσωτερικό PING + Αναπαραγωγή από τη λίστα συνομιλιών. + Παρακαλώ ζήτησε από την επαφή σου να ενεργοποιήσει τις κλήσεις. + Παρακαλώ ζήτησε από την επαφή σου να ενεργοποιήσει τα φωνητικά μηνύματα. + Έλεγξε ότι το κινητό και ο υπολογιστής είναι συνδεδεμένοι στο ίδιο τοπικό δίκτυο και ότι το τείχος προστασίας του υπολογιστή επιτρέπει τη σύνδεση.\nΕνημέρωσε τους προγραμματιστές για τυχόν άλλα προβλήματα. + Έλεγξε ότι ο σύνδεσμος SimpleX είναι σωστός. + Έλεγξε ότι χρησιμοποιείς το σωστό σύνδεσμο ή ζήτησε από την επαφή σου να σου στείλει έναν άλλο. + Έλεγξε τη σύνδεσή σου στο δίκτυο με %1$s και δοκίμασε ξανά. + Επιβεβαίωσε ότι οι ρυθμίσεις δικτύου είναι σωστές για αυτήν τη συσκευή. + Παρακαλώ επικοινώνησε με το διαχειριστή της ομάδας. + Εισήγαγε τη σωστή τρέχουσα φράση πρόσβασης. + Εισήγαγε τον προηγούμενο κωδικό μετά την επαναφορά του αντιγράφου ασφαλείας της βάσης δεδομένων. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί. + Μείωσε το μέγεθος του μηνύματος και απέστειλέ το ξανά. + Μείωσε το μέγεθος του μηνύματος ή αφαίρεσε τα αρχεία πολυμέσων και απέστειλέ το ξανά. + Παρακαλώ θυμήσου ή αποθήκευσε το με ασφάλεια - δεν υπάρχει τρόπος να ανακτήσεις έναν χαμένο κωδικό! + Παρακαλώ ανάφερέ το στους προγραμματιστές. + Παρακαλώ ανάφερέ το στους προγραμματιστές: \n%s + Παρακαλώ ανάφερέ το στους προγραμματιστές: \n%s\n\nΠροτείνεται η επανεκκίνηση της εφαρμογής. + Παρακαλώ επανεκκίνησε την εφαρμογή. + Αποθήκευσε τη φράση πρόσβασης σε ασφαλές μέρος, καθώς ΔΕΝ θα μπορείς να έχεις πρόσβαση στη συνομιλία αν τη χάσεις. + Αποθήκευσε τη φράση πρόσβασης σε ασφαλές μέρος, καθώς ΔΕΝ θα μπορείς να την αλλάξεις σε περίπτωση απώλειας. + Παρακαλώ δοκίμασε αργότερα. + Ενημέρωσε την εφαρμογή και επικοινώνησε με τους προγραμματιστές. + Παρακαλώ περίμενε μέχρι οι διαχειριστές της ομάδας να εξετάσουν το αίτημά σου για συμμετοχή στην ομάδα. + Παρακαλώ, περίμενε ενώ το αρχείο φορτώνεται από το συνδεδεμένο κινητό + Διεπαφή στα Πολωνικά + Μετέφερε + θύρα%d + Προετοιμασία λήψης + Προετοιμασία μεταφόρτωσης + Διατήρηση του τελευταίου πρόχειρου μηνύματος, με τα συνημμένα. + Προκαθορισμένος διακομιστής + Διεύθυνση προκαθορισμένου διακομιστή + Προκαθορισμένοι διακομιστές + Προκαθορισμένοι διακομιστές + Προεπισκόπηση + Προηγούμενοι συνδεδεμένοι διακομιστές + Προστασία της ιδιωτικότητας των πελατών σου. + Πολιτική απορρήτου και όροι χρήσης. + Επαναπροσδιορισμός της ιδιωτικότητας + Απόρρητο & ασφάλεια + Οι ιδιωτικές συνομιλίες, οι ομάδες και οι επαφές σου δεν είναι προσβάσιμες στους χειριστές του διακομιστή. + Ιδιωτικά ονόματα αρχείων + Ιδιωτικά ονόματα αρχείων πολυμέσων. + Δρομολόγηση ιδιωτικών μηνυμάτων 🚀 + ΔΡΟΜΟΛΟΓΗΣΗ ΙΔΙΩΤΙΚΩΝ ΜΗΝΥΜΑΤΩΝ + Ιδιωτικές σημειώσεις + Ιδιωτικές σημειώσεις + Ιδιωτικές ειδοποιήσεις + Ιδιωτική δρομολόγηση + Σφάλμα ιδιωτικής δρομολόγησης + Λήξη χρονικού ορίου ιδιωτικής δρομολόγησης + Προφίλ και συνδέσεις διακομιστή + εικόνα προφίλ + θέση για εικόνα προφίλ + Εικόνες προφίλ + Όνομα προφίλ: + Κωδικός προφίλ + Θέμα προφίλ + Η ενημέρωση του προφίλ θα σταλεί στις επαφές σου. + Απαγόρευση κλήσεων ήχου/βίντεο. + Απαγόρευση της μη αναστρέψιμης διαγραφής μηνυμάτων. + Απαγόρευση αντιδράσεων σε μήνυμα. + Απαγόρευση αντιδράσεων σε μηνύματα. + Απαγόρευση αναφοράς μηνυμάτων στους διαχειριστές. + Απαγόρευση αποστολής άμεσων μηνυμάτων στα μέλη. + Απαγόρευση αποστολής μηνυμάτων που εξαφανίζονται. + Απαγόρευση αποστολής μηνυμάτων που εξαφανίζονται. + Απαγόρευση αποστολής αρχείων και πολυμέσων. + Απαγόρευση αποστολής αρχείων και πολυμέσων. + Απαγόρευση αποστολής συνδέσμων SimpleX + Απαγόρευση αποστολής φωνητικών μηνυμάτων. + Απαγόρευση αποστολής φωνητικών μηνυμάτων. + Προστασία οθόνης εφαρμογής + Προστασία διεύθυνσης IP + Προστάτεψε τα προφίλ συνομιλίας σου με έναν κωδικό! + Προστάτεψε τη διεύθυνση IP σου από τα κέντρα διαβίβασης μηνυμάτων που επιλέγουν οι επαφές σου.\nΕνεργοποίησε την επιλογή στις ρυθμίσεις *Δίκτυο και διακομιστές*. + Χρονικό όριο πρωτοκόλλου + Χρονικό όριο πρωτοκόλλου + Χρονικό όριο πρωτοκόλλου ανά KB + Μέσω διακομιστή μεσολάβησης + Διακομιστές μέσω proxy + Πιστοποίηση διακομιστή μεσολάβησης + Κωδικός QR + κβαντο-ανθεκτική κρυπτογράφηση e2e + Κβαντο-ανθεκτική κρυπτογράφηση + Τυχαία + Η τυχαία φράση πρόσβασης αποθηκεύεται στις ρυθμίσεις ως απλό κείμενο.\nΜπορείς να την αλλάξεις αργότερα. + Αξιολόγησε την εφαρμογή + Προσβάσιμες γραμμές εργαλείων εφαρμογής + Προσβάσιμη γραμμή εργαλείων συνομιλίας + Προσβάσιμη γραμμή εργαλείων συνομιλίας + Διάβασε περισσότερα + Οι αναφορές παράδοσης είναι απενεργοποιημένες + απάντηση που παραλήφθηκε… + Παραλήφθηκε στις + Παραλήφθηκε στις: %s + επιβεβαίωση που παραλήφθηκε… + Μήνυμα που παραλήφθηκε + Μήνυμα που παραλήφθηκε + Μηνύματα που παραλήφθηκαν + παραλήφθηκε, απαγορεύεται + Παραλήφθηκε απάντηση + Σύνολο που παραλήφθηκε + Σφάλματα παραλαβής + Η διεύθυνση παραλαβής θα αλλάξει σε διαφορετικό διακομιστή. Η αλλαγή διεύθυνσης θα ολοκληρωθεί μετά την σύνδεση του αποστολέα. + Λήψη ταυτόχρονης πρόσβασης + η λήψη αρχείων δεν υποστηρίζεται ακόμη + Η λήψη αρχείων θα διακοπεί. + Λήψη μηνυμάτων… + Λήψη μέσω + Πρόσφατο ιστορικό και βελτιωμένο μποτ καταλόγου. + Ο/Οι παραλήπτης/ες δεν μπορούν να δουν από ποιον προέρχεται αυτό το μήνυμα. + Οι παραλήπτες βλέπουν τις ενημερώσεις καθώς τις πληκτρολογείς. + Επανασύνδεση + Επανασύνδεσε όλους τους συνδεδεμένους διακομιστές για να επιβάλεις την παράδοση μηνυμάτων. Χρησιμοποιεί επιπλέον κίνηση. + Επανασύνδεση όλων των διακομιστών + Επανασύνδεση διακομιστή; + Επανασύνδεση διακομιστών; + Επανασύνδεση διακομιστή για να επιβληθεί η παράδοση μηνυμάτων. Χρησιμοποιεί επιπλέον κίνηση. + Η εγγραφή ενημερώθηκε στις + Η εγγραφή ενημερώθηκε στις: %s + Εγγραφή φωνητικού μηνύματος + Μειωμένη χρήση μπαταρίας + Ανανέωση + Απόρριψη + Απόρριψη + Απόρριψη + Απόρριψη αιτήματος επαφής + απορρίφθηκε + απορρίφθηκε + απορριφθείσα κλήση + Απορριφθείσα κλήση + Απόρριψη μέλους; + Ο διακομιστής αναμετάδοσης χρησιμοποιείται μόνο αν είναι απαραίτητο. Οι άλλοι μπορούν να δουν τη διεύθυνση IP σου. + Ο διακομιστής αναμετάδοσης προστατεύει τη διεύθυνση IP σου, αλλά μπορεί να παρακολουθεί τη διάρκεια της κλήσης. + Υπενθύμιση αργότερα + Απομακρυσμένα κινητά τηλέφωνα + Κατάργηση + Κατάργηση + Κατάργηση και διαγραφή μηνυμάτων + Κατάργηση αρχείου αρχειοθέτησης; + καταργήθηκε + καταργήθηκε %1$s + διεγραμμένη διεύθυνση επαφής + αφαιρέθηκε από την ομάδα + αφαιρέθηκε η φωτογραφία προφίλ + σε αφαίρεσε + Κατάργηση εικόνας + Κατάργηση παρακολούθησης συνδέσμων + Κατάργηση μέλους + Κατάργηση μέλους + Κατάργηση μέλους; + Κατάργηση μελών; + Κατάργηση φράσης πρόσβασης από το Keystore; + Κατάργηση φράσης πρόσβασης από τις ρυθμίσεις; + Κατάργηση μηνυμάτων και μπλοκάρισμα μελών. + Επαναδιαπραγμάτευση + Επαναδιαπραγμάτευση κρυπτογράφησης + Επαναδιαπραγμάτευση κρυπτογράφησης; + Επανάληψη στην οθόνη + Επανάληψη αιτήματος σύνδεσης; + Επανάληψη λήψης + Επανάληψη εισαγωγής + Επανάληψη αιτήματος συμμετοχής; + Επανάληψη μεταφόρτωσης + Απάντησε + Ανέφερε + Αναφορά περιεχομένου: μόνο οι διαχειριστές της ομάδας θα το δουν. + Η αναφορά μηνυμάτων απαγορεύεται σε αυτήν την ομάδα. + Αναφορά προφίλ μέλους: μόνο οι διαχειριστές της ομάδας θα το δουν. + Άλλη αναφορά: μόνο οι διαχειριστές της ομάδας θα το δουν. + Αιτία αναφοράς; + Αναφορά: %s + Αναφορές + Η αναφορά εστάλη στους διαχειριστές + Επανεκκίνηση + Αναφορά spam: μόνο οι διαχειριστές της ομάδας θα το δουν. + Αναφορά παραβίασης κανόνων: μόνο οι διαχειριστές της ομάδας θα τη δουν. + αιτήσου σύνδεση από την ομάδα %1$s + αιτήσου να συνδεθείς + το αίτημα αποστέλλεται + το αίτημα συμμετοχής απορρίφθηκε + Απαιτείται + Επανέφερε + Επαναφορά + Επαναφορά όλων των υποδείξεων + Επαναφορά όλων των στατιστικών + Επαναφορά όλων των στατιστικών; + Επαναφορά χρώματος + Επαναφορά χρωμάτων + Επαναφορά στο θέμα της εφαρμογής + Επαναφορά στις προεπιλογές + Επαναφορά στο θέμα χρήστη + Επανεκκίνηση συνομιλίας + Επανεκκίνησε την εφαρμογή για να δημιουργήσεις ένα νέο προφίλ συνομιλίας. + Επανεκκίνησε την εφαρμογή για να χρησιμοποιήσεις την εισαγώμενη βάση δεδομένων. + Επαναφορά + Επαναφορά αντιγράφου ασφαλείας βάσης δεδομένων + Επαναφορά αντιγράφου ασφαλείας βάσης δεδομένων; + Σφάλμα επαναφοράς βάσης δεδομένων + Επανέλαβε + Αποκάλυψε + ανασκόπηση + Προϋποθέσεις ελέγχου + ελέγχθηκε από τους διαχειριστές + Έλεγχος μελών ομάδας + Έλεγχος αργότερα + Έλεγχος μελών + Έλεγχος μελών πριν την αποδοχή τους (knocking). + Ανάκληση + Ανάκληση αρχείου + Ανάκληση αρχείου; + Ρόλος + ΕΚΚΙΝΗΣΗ ΣΥΝΟΜΙΛΙΑΣ + Εκτελείται όταν η εφαρμογή είναι ανοιχτή + Ασφαλής λήψη αρχείων + Ασφαλέστερες ομάδες + %s και %s + %s και %s συνδέθηκαν + %s στις %s + Αποθήκευσε + Αποθήκευση + Αποθήκευση + Αποθήκευση ρυθμίσεων εισόδου; + Αποθήκευση και ειδοποίηση επαφής + Αποθήκευση και ειδοποίηση επαφών + Αποθήκευση και ειδοποίηση μελών ομάδας + Αποθήκευση και επανασύνδεση + Αποθήκευση και ενημέρωση προφίλ ομάδας + αποθηκευμένο + Αποθηκευμένο + Αποθηκευμένο από + αποθηκευμένο από %s + Αποθηκευμένο μήνυμα + Οι αποθηκευμένοι διακομιστές WebRTC ICE θα αφαιρεθούν. + Αποθήκευση προφίλ ομάδας + Αποθήκευση λίστας + Αποθήκευση φράσης πρόσβασης και άνοιγμα συνομιλίας + Αποθήκευση φράσης πρόσβασης στο Keystore + Αποθήκευση φράσης πρόσβασης στις ρυθμίσεις + Αποθήκευση προτιμήσεων; + Αποθήκευση κωδικού προφίλ + Αποθήκευση διακομιστών + Αποθήκευση διακομιστών; + Αποθήκευση ρυθμίσεων; + Αποθήκευση ρυθμίσεων διεύθυνσης SimpleX + Αποθήκευση μηνύματος καλωσορίσματος; + Αποθήκευση %1$s μηνυμάτων + Κλιμάκωση στην οθόνη + Σάρωση κωδικού + Σάρωση από κινητό + (σάρωσε ή επικόλλησε από το πρόχειρο) + Σάρωση / Επικόλληση συνδέσμου + Σάρωσε τον κωδικό QR από τον υπολογιστή + %s συνδέθηκε + %s (τρέχον) + %s κατέβηκαν + αναζήτηση + Η γραμμή αναζήτησης δέχεται συνδέσμους πρόσκλησης. + Αναζήτηση ή επικόλληση συνδέσμου SimpleX + Δευτερεύων + Ασφαλής + ο κωδικός ασφαλείας άλλαξε + Επιλογή + Επέλεξε + Επέλεξε προφίλ συνομιλίας + Επέλεξε επαφές + Οι επιλεγμένες προτιμήσεις συνομιλίας απαγορεύουν αυτό το μήνυμα. + Επιλέχθηκαν %d + Επέλεξε τους χειριστές δικτύου που θέλεις να χρησιμοποιήσεις. + Αυτοκαταστροφή + Κωδικός αυτοκαταστροφής + Κωδικός αυτοκαταστροφής + Ο κωδικός αυτοκαταστροφής άλλαξε! + Ο κωδικός αυτοκαταστροφής ενεργοποιήθηκε! + Απέστειλε + Απέστειλε + Στείλε ένα ζωντανό μήνυμα - θα ενημερώνεται για τον παραλήπτη ή τους παραλήπτες καθώς το πληκτρολογείς. + Αποστολή αιτήματος επαφής; + ΑΠΟΣΤΟΛΗ ΑΝΑΦΟΡΩΝ ΠΑΡΑΔΟΣΗΣ ΣΕ + Αποστολή άμεσου μηνύματος + Στείλε άμεσο μήνυμα για να συνδεθείς + Αποστολή μηνύματος που εξαφανίζεται + Ο αποστολέας ακύρωσε τη μεταφορά αρχείων. + Ο αποστολέας ενδέχεται να έχει διαγράψει το αίτημα σύνδεσης. + Σφάλματα αποστολής + αποτυχία αποστολής + Η αποστολή αναφορών παράδοσης θα είναι ενεργοποιημένη για όλες τις επαφές. + Η αποστολή αναφορών παράδοσης θα είναι ενεργοποιημένη για όλες τις επαφές σε όλα τα ορατά προφίλ συνομιλίας. + η αποστολή αρχείων δεν υποστηρίζεται ακόμη + Η αποστολή του αρχείου θα διακοπεί. + Η αποστολή αναφορών είναι απενεργοποιημένη για %d επαφές + Η αποστολή αναφορών είναι απενεργοποιημένη για %d ομάδες + Η αποστολή αναφορών είναι ενεργοποιημένη για %d επαφές + Η αποστολή αναφορών είναι ενεργοποιημένη για %d ομάδες + Αποστέλλεται μέσω + Αποστολή προεπισκόπησης συνδέσμων + Αποστολή ζωντανού μηνύματος + Αποστολή Μηνύματος + Στείλε μηνύματα απευθείας όταν η διεύθυνση IP είναι προστατευμένη και ο διακομιστής σου ή ο διακομιστής προορισμού δεν υποστηρίζει ιδιωτική δρομολόγηση. + Στείλε μηνύματα απευθείας όταν ο διακομιστής σου ή ο διακομιστής προορισμού δεν υποστηρίζει ιδιωτική δρομολόγηση. + Στείλε μήνυμα για να ενεργοποιήσεις τις κλήσεις. + Αποστολή ιδιωτικών αναφορών + Στείλε ερωτήσεις και ιδέες + Αποστολή αναφορών + Αποστολή αιτήματος + Αποστολή αιτήματος χωρίς μήνυμα + αποστολή για σύνδεση + Αποστολή εώς και 100 τελευταίων μηνυμάτων σε νέα μέλη. + Στείλε μας ένα mail + Στείλε τα προσωπικά σου σχόλια στις ομάδες. + στάλθηκε + Στάλθηκε στις + Στάλθηκε στις: %s + Στάλθηκε απευθείας + Απεσταλμένο μήνυμα + Απεσταλμένο μήνυμα + Απεσταλμένα μηνύματα + Τα αποσταλμένα μηνύματα θα διαγραφούν μετά από καθορισμένο χρονικό διάστημα. + Απεσταλμένη απάντηση + Σύνολο απεσταλμένων + Αποστέλλεται στην επαφή σου μετά τη σύνδεση. + Αποστολή μέσω διακομιστή μεσολάβησης + Διακομιστής + Ο διακομιστής προστέθηκε στο χειριστή %s. + Διεύθυνση διακομιστή + Η διεύθυνση του διακομιστή δεν είναι συμβατή με τις ρυθμίσεις δικτύου. + Η διεύθυνση του διακομιστή δεν είναι συμβατή με τις ρυθμίσεις δικτύου: %1$s. + Ο χειριστής του διακομιστή άλλαξε. + Χειριστές διακομιστή + Αλλαγή πρωτοκόλλου διακομιστή. + πληροφορίες ουράς διακομιστή: %1$s\n\nτελευταίο ληφθέν μήνυμα: %2$s + Ο διακομιστής απαιτεί εξουσιοδότηση για τη δημιουργία ουρών, έλεγξε τον κωδικό. + Ο διακομιστής απαιτεί εξουσιοδότηση για ανέβασμα αρχείων, έλεγξε τον κωδικό. + ΔΙΑΚΟΜΙΣΤΕΣ + Πληροφορίες διακομιστών + Θα γίνει επαναφορά στα στατιστικά στοιχεία των διακομιστών - αυτή η ενέργεια δεν μπορεί να αναιρεθεί! + Η δοκιμή του διακομιστή απέτυχε! + Η έκδοση του διακομιστή δεν είναι συμβατή με τις ρυθμίσεις δικτύου. + Η έκδοση του διακομιστή δεν είναι συμβατή με την εφαρμογή σου: %1$s. + Κωδικός συνεδρίας + Όρισε σε 1 ημέρα + Όρισε το όνομα συνομιλίας… + Όρισε το όνομα επαφής + Όρισε το όνομα επαφής… + Όρισε τη φράση πρόσβασης της βάσης δεδομένων + Όρισε το προεπιλεγμένο θέμα + Όρισε τις προτιμήσεις ομάδας + Όρισέ τον αντί για την πιστοποίηση συστήματος. + Όρισε την εισαγωγή μέλους + Όρισε τη λήξη των μηνυμάτων στις συνομιλίες. + ορίστε νέα διεύθυνση επαφής + όρισε νέα εικόνα προφίλ + Όρισε κωδικό πρόσβασης + Όρισε φράση πρόσβασης + Όρισε φράση πρόσβασης για εξαγωγή + Όρισε το βιογραφικό του προφίλ και το μήνυμα καλωσορίσματος. + Όρισε το εμφανιζόμενο μήνυμα για τα νέα μέλη! + Ρυθμίσεις + Ρυθμίσεις + ΡΥΘΜΙΣΕΙΣ + Όρισε τη φράση πρόσβασης της βάσης δεδομένων + Διαμόρφωση εικόνων προφίλ + Διαμοίρασε + Διαμοίρασε το σύνδεσμο 1-χρήσης + Διαμοίρασε το σύνδεσμο 1-χρήσης με ένα φίλο + Διαμοιρασμός διεύθυνσης + Δημόσιος διαμοιρασμός διεύθυνσης + Διαμοιρασμός διεύθυνσης με τις επαφές; + Διαμοιρασμός αρχείου… + Διαμοιρασμός συνδέσμου + Διαμοιρασμός πολυμέσων… + Διαμοιρασμός μηνύματος… + Διαμοιρασμός παλιάς διεύθυνσης + Διαμοιρασμός παλιού συνδέσμου + Διαμοιρασμός προφίλ + Διαμοιρασμός διεύθυνσης SimpleX σε εφαρμογές κοινωνικής δικτύωσης. + Διαμοιρασμός αυτού του συνδέσμου 1-χρήσης + Διαμοιρασμός με τις επαφές + Διαμοιρασμός της διεύθυνσής σου + Σύντομη περιγραφή: + Σύντομος σύνδεσμος + Σύντομη διεύθυνση SimpleX + Εμφάνιση + Εμφάνιση: + Εμφάνιση λίστας μηνυμάτων σε νέο παράθυρο + Εμφάνιση κονσόλας τερματικού σε νέο παράθυρο + Εμφάνιση επαφής και μηνύματος + Εμφάνιση επιλογών για προγραμματιστές + Εμφάνιση πληροφοριών για + Εμφάνιση εσωτερικών σφαλμάτων + Εμφάνιση τελευταίων μηνυμάτων + Εμφάνιση κατάστασης μηνύματος + Εμφάνιση μόνο της επαφής + Εμφάνιση ποσοστού + Εμφάνιση προεπισκόπησης + Εμφάνιση κωδικού QR + Εμφάνιση αργών κλήσεων API + Απενεργοποίηση + Απενεργοποίηση; + SImpleX + SimpleX διεύθυνση + Διεύθυνση SimpleX + Η διεύθυνση SimpleX και οι σύνδεσμοι 1-χρήσης είναι ασφαλές να διαμοιράζονται μέσω οποιασδήποτε εφαρμογής ανταλλαγής μηνυμάτων. + Διεύθυνση SimpleX ή σύνδεσμος 1-χρήσης; + Το SimpleX δεν μπορεί να λειτουργήσει στο παρασκήνιο. Θα λαμβάνεις τις ειδοποιήσεις μόνο όταν η εφαρμογή είναι σε λειτουργία. + Σύνδεσμος καναλιού SimpleX + Η SimpleX Chat και η Flux σύναψαν συμφωνία για την ενσωμάτωση των διακομιστών που λειτουργεί η Flux, στην εφαρμογή. + Κλήσεις SimpleX Chat + Μηνύματα SimpleX Chat + Η ασφάλεια του SimpleX Chat ελέγχθηκε από την Trail of Bits. + Υπηρεσία SimpleX Chat + Διεύθυνση επικοινωνίας SimpleX + Σύνδεσμος ομάδας SimpleX + Σύνδεσμοι SimpleX + Σύνδεσμοι SimpleX + Οι σύνδεσμοι SimpleX απαγορεύονται. + Οι σύνδεσμοι SimpleX δεν επιτρέπονται + SimpleX Lock + SimpleX Lock + Λειτουργία SimpleX Lock + Το SimpleX Lock δεν είναι ενεργοποιημένο! + Το SimpleX Lock είναι ενεργοποιημένο + SimpleX Logo + simplexmq: v%s (%2s) + Πρόσκληση 1-χρήσης SimpleX + Πρωτόκολλα SimpleX που έχουν ελεγχθεί από την Trail of Bits. + Σύνδεσμος αναμεταδότη SimpleX + SimpleX Team + Απλοποιημένη ανώνυμη λειτουργία + %s δεν έχει επαληθευτεί + %s έχει επαληθευτεί + Μέγεθος + Παράλειψη πρόσκλησης μελών + Παραλειπόμενα μηνύματα + Παράλειψη αυτής της έκδοσης + Αργή λειτουργία + Μικρές ομάδες (μέγιστο 20 άτομα) + Διακομιστής SMP + Διακομιστές SMP + Διακομιστής μεσολάβησης SOCKS + ΔΙΑΚΟΜΙΣΤΗΣ ΜΕΣΟΛΑΒΗΣΗΣ SOCKS + Ρυθμίσεις διακομιστή μεσολάβησης SOCKS + Απαλό + Κάποιο/α αρχείο/α δεν εξήχθησαν + Κατά την εισαγωγή προέκυψαν ορισμένα μη κρίσιμα σφάλματα: + Ορισμένοι διακομιστές απέτυχαν στη δοκιμή: + Ήχος σε σίγαση + Spam + Spam + Ηχείο + Απενεργοποίηση ηχείου + Εεργοποίηση ηχείου + Τετράγωνο, κύκλος ή οτιδήποτε μεταξύ τους. + %s: %s + %s, %s και %d μέλη + %s, %s και %d άλλα μέλη συνδεδεμένα + %s, %s και %s συνδεδεμένα + %s δευτερόλεπτο/α + %s διακομιστές + Σταθερή + τυποποιημένη κρυπτογράφηση από άκρη-σε-άκρη + Αστέρι στο GitHub + Εκκίνηση συνομιλίας + Εκκίνηση συνομιλίας; + εκκινεί… + Εκκινεί από %s. + Εκκινεί από %s.\nΌλα τα δεδομένα παραμένουν ιδιωτικά στη συσκευή σου. + Εκκίνηση νέας συνομιλίας + Εκκινεί περιοδικά + Στατιστικά + Διακοπή + Διακοπή + Διακοπή συνομιλίας + Διακοπή συνομιλίας; + Διέκοψε τη συνομιλία για να εξάγεις, να εισάγεις ή να διαγράψεις τη βάση δεδομένων συνομιλιών. Δεν θα μπορείς να λαμβάνεις και να στέλνεις μηνύματα ενώ η συνομιλία έχει διακοπεί. + Διακοπή αρχείου + Διακοπή συνομιλίας + Διακοπή λήψης αρχείου; + Διακοπή αποστολής αρχείου; + Διακοπή διαμοιρασμού + Διακοπή διαμοιρασμού διεύθυνσης; + διαγράμμιση + Έντονο + Υποβολή + Εγγεγραμμένος + Σφάλματα εγγραφής + Η εγγραφή αγνοήθηκε + %s ανεβασμένα + Υποστήριξη bluetooth και άλλων βελτιώσεων. + ΥΠΟΣΤΗΡΙΞΗ SIMPLEX CHAT + Ενάλλαξε + Εναλλαγή ήχου και βίντεο κατά τη διάρκεια της κλήσης. + Αλλαγή προφίλ συνομιλίας για προσκλήσεις 1-χρήσης. + Σύστημα + Σύστημα + Σύστημα + Σύστημα + Αυθεντικοποίηση συστήματος + Λειτουργία συστήματος + Ουρά + Πάτα το κουμπί + Επαλήθευση κωδικού στο κινητό + Επαλήθευση κωδικού με υπολογιστή + Επαλήθευση σύνδεσης + Επαλήθευση συνδέσεων + Επαλήθευση ασφάλειας σύνδεσης + Επαλήθευση φράσης πρόσβασης της βάσης δεδομένων + Επαλήθευση φράσης πρόσβασης + Επαλήθευση κωδικού ασφαλείας + μέσω %1$s + Μέσω περιηγητή + μέσω του συνδέσμου διεύθυνσης επαφής + μέσω συνδέσμου ομάδας + μέσω συνδέσμου 1-χρήσης + μέσω αναμεταδότη + Μέσω ασφαλούς κβαντο-ανθεκτικού πρωτοκόλλου + βίντεο + Βίντεο + Βίντεο + βιντεοκλήση + Βιντεοκλήση + βιντεοκλήση (χωρίς κρυπτογράφηση e2e) + Βίντεο απενεργοποιημένο + Βίντεο ενεργοποιημένο + Βίντεο και αρχεία εώς 1gb + Βίντεο απεστάλη + Το βίντεο θα ληφθεί όταν η επαφή σου ολοκληρώσει τη μεταφόρτωσή του. + Το βίντεο θα ληφθεί όταν η επαφή σου είναι συνδεδεμένη, παρακαλώ περίμενε ή έλεγξε αργότερα! + Δες τους όρους + Προβολή κωδικού ασφαλείας + Προβολή ενημερωμένων συνθηκών + Ορατό ιστορικό + Φωνητικό μήνυμα + Φωνητικό μήνυμα… + Φωνητικό μήνυμα (%1$s) + Φωνητικά μηνύματα + Φωνητικά μηνύματα + Τα φωνητικά μηνύματα απαγορεύονται. + Τα φωνητικά μηνύματα απαγορεύονται σε αυτήν τη συνομιλία. + Τα φωνητικά μηνύματα δεν επιτρέπονται + Τα φωνητικά μηνύματα απαγορεύονται! + - φωνητικά μηνύματα εώς 5 λεπτά.\n- προσαρμοσμένος χρόνος εξαφάνισης.\n- ιστορικό επεξεργασίας. + αναμονή για απάντηση… + αναμονή για επιβεβαίωση… + Αναμονή για τον υπολογιστή… + Αναμονή για το αρχείο + Αναμονή για την εικόνα + Αναμονή για την εικόνα + Αναμονή σύνδεσης κινητού: + Αναμονή για το βίντεο + Αναμονή για το βίντεο + Χρωματική έμφαση ταπετσαρίας + Φόντο ταπετσαρίας + θέλει να συνδεθεί μαζί σου! + Προειδοποίηση: η έναρξη συνομιλίας σε πολλαπλές συσκευές δεν υποστηρίζεται και θα προκαλέσει σφάλματα στην παράδοση των μηνυμάτων. + Προειδοποίηση: ενδέχεται να χάσεις ορισμένα δεδομένα! + Διακομιστές WebRTC ICE + Ιστοσελίδα + Δεν αποθηκεύουμε καμία από τις επαφές ή τα μηνύματά σου (αφού παραδοθούν) στους διακομιστές. + εβδομάδες + Καλωσόρισες! + Καλωσόρισες %1$s! + Μήνυμα καλωσορίσματος + Μήνυμα καλωσορίσματος + Μήνυμα καλωσορίσματος + Το μήνυμα καλωσορίσματος είναι πολύ μεγάλο + Καλωσόρισε τις επαφές σου 👋 + Τι νέο υπάρχει + Όταν η εφαρμογή είναι σε λειτουργία + Όταν είναι διαθέσιμο + Κατά τη σύνδεση κλήσεων ήχου και βίντεο. + Όταν η IP είναι κρυφή + Όταν είναι ενεργοποιημένοι περισσότεροι από ένας χειριστές, κανένας από αυτούς δεν διαθέτει μεταδεδομένα για να μάθει ποιος επικοινωνεί με ποιον. + Όταν κάποιος ζητήσει να συνδεθεί, μπορείς να αποδεχτείς ή να απορρίψεις το αίτημα. + Όταν μοιράζεσε ένα ανώνυμο προφίλ με κάποιον, αυτό το προφίλ θα χρησιμοποιείται για τις ομάδες στις οποίες σε προσκαλούν. + WiFi + Θα ενεργοποιηθεί στις άμεσες συνομιλίες! + Ενσύρματο ethernet + Με κρυπτογραφημένα αρχεία και μέσα. + Με προαιρετικό μήνυμα καλωσορίσματος. + Χωρίς Tor ή VPN, η διεύθυνση IP σου θα είναι ορατή στους διακομιστές αρχείων. + Χωρίς Tor ή VPN, η διεύθυνση IP σου θα είναι ορατή σε αυτούς τους XFTP αναμεταδότες:\n%1$s. + Με μειωμένη χρήση της μπαταρίας. + Με μειωμένη χρήση της μπαταρίας. + Λανθασμένη φράση πρόσβασης της βάσης δεδομένων + Λανθασμεο κλειδί ή άγνωστη σύνδεση - πιθανότατα αυτή η σύνδεση έχει διαγραφεί. + Λανθασμένο κλειδί ή άγνωστη διεύθυνση τμήματος αρχείου - πιθανότατα το αρχείο έχει διαγραφεί. + Λανθασμένη φράση πρόσβασης! + Διακομιστής XFTP + Διακομιστές XFTP + ναι + Ναι + Ναι + εσύ + ΕΣΥ + εσύ: %1$s + Αποδέχθηκες τη σύνδεση + αποδέχθηκες αυτό το μέλος + Επιτρέπεις + Έχεις ήδη ένα προφίλ συνομιλίας με το ίδιο όνομα εμφάνισης. Παρακαλώ επέλεξε ένα άλλο όνομα. + Είσαι ήδη συνδεδεμένος στο %1$s. + Ήδη συνδέεσαι μέσω αυτού του μοναδικού συνδέσμου! + Έχεις ήδη ενταχθεί στην ομάδα μέσω αυτού του συνδέσμου. + Είσαι προσκεκλημένος στην ομάδα + Είσαι προσκεκλημένος στην ομάδα + Είσαι προσκεκλημένος στην ομάδα. Αποδέξου την πρόσκληση για να συνδεθείς με τα μέλη της ομάδας. + Δεν είσαι συνδεδεμένος στον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτή τη σύνδεση (δεν υπάρχει συνδρομή). + Δεν είσαι συνδεδεμένος σε αυτούς τους διακομιστές. Για την παράδοση μηνυμάτων σε αυτούς, χρησιμοποιείται ιδιωτική δρομολόγηση. + είσαι παρατηρητής + είσαι παρατηρητής + μπλόκαρες %s + Μπορείς να το αλλάξεις στις ρυθμίσεις Εμφάνισης. + Μπορείς να διαμορφώσεις τους χειριστές στις ρυθμίσεις Δικτύου & διακομιστών. + Μπορείς να διαμορφώσεις τους διακομιστές μέσω των ρυθμίσεων. + Μπορείς να αντιγράψεις και να μειώσεις το μέγεθος του μηνύματος για να το στείλεις. + Μπορείς να το δημιουργήσεις αργότερα + Μπορείς να το ενεργοποιήσεις αργότερα μέσω των Ρυθμίσεων. + Μπορείς να τις ενεργοποιήσεις αργότερα μέσω των ρυθμίσεων απορρήτου και ασφάλειας της εφαρμογής. + Μπορείς να δοκιμάσεις ξανά. + Μπορείς να δοκιμάσεις ξανά. + Μπορείς να αποκρύψεις ή να σιγάσεις ένα προφίλ χρήστη - κράτησέ το πατημένο για να εμφανιστεί το μενού. + Μπορείς να το κάνεις ορατό στις επαφές σου στο SimpleX μέσω των Ρυθμίσεων. + Μπορείς να αναφέρεις εώς και %1$s μέλη ανά μήνυμα! + Μπορείς να στείλεις μηνύματα στην επαφή %1$s από τις αρχειοθετημένες επαφές. + Μπορείς να ορίσεις το όνομα της σύνδεσης για να θυμάσε με ποιον μοιράστηκες το σύνδεσμο. + Μπορείς να μοιραστείς ένα σύνδεσμο ή έναν κωδικό QR - οποιοσδήποτε θα μπορεί να συμμετάσχει στην ομάδα. Δεν θα χάσεις μέλη της ομάδας αν τον διαγράψεις αργότερα. + Μπορείς να μοιραστείς αυτήν τη διεύθυνση με τις επαφές σου για να τους επιτρέψεις να συνδεθούν με την επαφή %s. + Μπορείς να διαμοιραστείς τη διεύθυνσή σου ως σύνδεσμο ή κωδικό QR - οποιοσδήποτε θα μπορεί να συνδεθεί μαζί σου. + Μπορείς να ξεκινήσεις τη συνομιλία μέσω της εφαρμογής Ρυθμίσεις / Βάση δεδομένων ή επανεκκινώντας την εφαρμογή. + Μπορείς ακόμα να δεις τη συνομιλία με την επαφή %1$s, στη λίστα των συνομιλιών. + Δεν μπορείς να στείλεις μηνύματα! + Μπορείς να ενεργοποιήσεις το SimpleX Lock μέσω των Ρυθμίσεων. + Μπορείς να χρησιμοποιήσεις σύνταξη markdown για να μορφοποιήσεις τα μηνύματα: + Μπορείς να δεις ξανά το σύνδεσμο πρόσκλησης στις λεπτομέρειες σύνδεσης. + Μπορείς να δείς τις αναφορές σου στη Συνομιλία με τους διαχειριστές. + άλλαξες διεύθυνση + άλλαξες διεύθυνση για %s + άλλαξες ρόλο για τον εαυτό σου σε %s + άλλαξες το ρόλο του μέλους %s σε %s + Έχεις τον έλεγχο της συνομιλίας σου! + Δεν ήταν δυνατή η επαλήθευση. Παρακαλώ, δοκίμασε ξανά. + Εσύ αποφασίζεις ποιος μπορεί να συνδεθεί. + Έχεις ήδη ζητήσει σύνδεση μέσω αυτής της διεύθυνσης! + Δεν έχεις συνομιλίες + Πρέπει να εισάγεις τη φράση πρόσβασης κάθε φορά που ξεκινά η εφαρμογή - δεν αποθηκεύεται στη συσκευή. + Προσκάλεσες μία επαφή + Εντάχθηκες σε αυτήν την ομάδα + Έχεις ενταχθεί σε αυτή την ομάδα. Σύνδεση με το μέλος που σε προσκάλεσε. + αποχώρησες + αποχώρησες + Μπορείς να μεταφέρεις την εξαγώμενη βάση δεδομένων. + Μπορείς να αποθηκεύσεις το εξαγώμενο αρχείο. + Πρέπει να χρησιμοποιήσεις την πιο πρόσφατη έκδοση της βάσης δεδομένων συνομιλιών σου σε ΜΟΝΟ μία συσκευή, διαφορετικά ενδέχεται να σταματήσεις να λαμβάνεις μηνύματα από ορισμένες επαφές. + Πρέπει να επιτρέψεις στην επαφή σου να σε καλέσει για να μπορείς να την καλέσεις πίσω. + Για να μπορείς να στέλνεις φωνητικά μηνύματα, πρέπει να επιτρέψεις στην επαφή σου να στέλνει φωνητικά μηνύματα. + Η επαγγελματική σου επαφή + Η κλήσεις σου + Η βάση δεδομένων συνομιλιών σου + Η βάση δεδομένων συνομιλιών σου δεν είναι κρυπτογραφημένη - όρισε μία φράση πρόσβασης για να την προστατεύσεις. + Τα προφίλ συνομιλιών σου + Το προφίλ συνομιλίας σου θα σταλεί στα μέλη της συνομιλίας. + Το προφίλ συνομιλίας σου θα σταλεί στα μέλη της ομάδας. + Η σύνδεσή σου μεταφέρθηκε στο προφίλ %s, αλλά προέκυψε σφάλμα κατά την εναλλαγή του. + Η επαφή σου + Η επαφή σου πρέπει να είναι συνδεδεμένη στο διαδίκτυο για να ολοκληρωθεί η σύνδεση.\nΜπορείς να ακυρώσεις αυτήν τη σύνδεση και να καταργήσεις την επαφή (και να δοκιμάσεις αργότερα με έναν νέο σύνδεσμο). + Οι επαφές σου + Οι επαφές σου μπορούν να επιτρέψουν την πλήρη διαγραφή μηνυμάτων. + Τα διαπιστευτήριά σου ενδέχεται να αποσταλούν χωρίς κρυπτογράφηση. + Η τρέχουσα βάση δεδομένων συνομιλιών σου θα ΔΙΑΓΡΑΦΕΙ και θα ΑΝΤΙΚΑΤΑΣΤΑΘΕΙ με την εισαγώμενη.\nΑυτή η ενέργεια δεν μπορεί να αναιρεθεί - το προφίλ, οι επαφές, τα μηνύματα και τα αρχεία σου θα χαθούν οριστικά. + Προσπαθείς να προσκαλέσεις μία επαφή με την οποία έχεις μοιραστεί ένα ανώνυμο προφίλ στην ομάδα στην οποία χρησιμοποιείς το κύριο προφίλ σου. + Χρησιμοποιείς ένα ανώνυμο προφίλ για αυτήν την ομάδα - για να αποφύγεις την κοινή χρήση του κύριου προφίλ σου, δεν επιτρέπεται η πρόσκληση επαφών. + Η ομάδα σου + Το προφίλ σου + Το προφίλ σου αποθηκεύεται στη συσκευή σου και κοινοποιείται μόνο στις επαφές σου. Οι διακομιστές της SimpleX δεν μπορούν να δουν το προφίλ σου. + Οι διακομιστές σου + διαμοιράστηκες ένα σύνδεσμο 1-χρήσης + διαμοιράστηκες ένα σύνδεσμο 1-χρήσης ανώνυμα + ξεμπλόκαρες %s + Θα συνδεθείς στην ομάδα όταν η συσκευή του διαχειριστή της ομάδας είναι συνδεδεμένη στο διαδίκτυο. Παρακαλώ περίμενε ή έλεγξε αργότερα! + Θα συνδεθείς όταν γίνει αποδεκτό το αίτημά σου για σύνδεση. Παρακαλώ περίμενε ή έλεγξε αργότερα! + Θα σου ζητηθεί να πραγματοποιήσεις έλεγχο ταυτότητας όταν ξεκινήσεις ή συνεχίσεις την εφαρμογή μετά από 30 δευτερόλεπτα στο παρασκήνιο. + Θα συνεχίσεις να λαμβάνεις κλήσεις και ειδοποιήσεις από τα προφίλ που έχεις σε σίγαση όταν αυτά θα είναι ενεργά. + Δεν θα λαμβάνεις πλέον μηνύματα από αυτήν τη συνομιλία. Το ιστορικό συνομιλιών θα διατηρηθεί. + Δεν θα λαμβάνεις πλέον μηνύματα από αυτήν την ομάδα. Το ιστορικό συνομιλιών θα διατηρηθεί. + Δεν θα χάσεις τις επαφές σου αν διαγράψεις αργότερα τη διεύθυνσή σου. + Μεγέθυνση + Όλα τα μηνύματα + Αρχεία + ΦΙλτράρισμα + Εικόνες + Σύνδεσμοι + Αναζήτηση αρχείων + Αναζήτηση εικόνων + Αναζήτηση συνδέσμων + Αναζήτηση βίντεο + Αναζήτηση φωνητικών μηνυμάτων + Βίντεο + Φωνητικά μηνύματα diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index b5e756aaad..c233d8eabc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -1732,7 +1732,7 @@ Descargar Reenviar Reenviado - Mensaje reenviado… + Reenviando mensaje… Los destinatarios no ven de quién procede este mensaje. Bluetooth Concurrencia en la recepción @@ -1906,7 +1906,7 @@ Las estadísticas de los servidores serán restablecidas. ¡No puede deshacerse! Descargado Servidor SMP - Aún no hay conexión directa, el mensaje es reenviado por el administrador. + Aún no hay conexión directa, los mensajes son reenviados por el administrador. Otros servidores SMP Otros servidores XFTP Escanear / Pegar enlace @@ -2391,7 +2391,7 @@ Error al aceptar el miembro ¿Guardar configuración? Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo. - has aceptado al miembro + has admitido al miembro pendiente de revisión por revisar Chat con administradores @@ -2419,7 +2419,7 @@ ¡No puedes enviar mensajes! Puedes ver tus informes en Chat con administradores has salido - te ha aceptado + te ha admitido Un miembro nuevo desea unirse al grupo. todos Chat con miembros @@ -2537,4 +2537,21 @@ La huella en la dirección del servidor de reenvío no coincide con el certificado: %1$s. Sin suscripciones No estás conectado al servidor usado para recibir mensajes de esta conexión (no suscrito). + Eliminar mensajes del miembro + ¿Eliminar mensajes del miembro? + Eliminar mensajes + Los mensajes del miembro serán eliminados. ¡No puede deshacerse! + Eliminar miembro y sus mensajes + Todos los mensajes + Archivos + Filtro + Imágenes + Enlaces + Buscar archivos + Buscar imágenes + Buscar enlaces + Buscar vídeos + Buscar mensajes de voz + Vídeos + Mensajes de voz diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index eba31ba788..5483becb91 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -2532,4 +2532,5 @@ اثر انگشت در نشانی سرور مقصد با گواهی مطابقت ندارد: ‎%1$s. اثر انگشت در نشانی سرور انتقال با گواهی مطابقت ندارد: ‎%1$s. اثر انگشت در نشانی سرور با گواهی مطابقت ندارد: ‎%1$s. + پاک کردن پیام کاربر diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 12b578edbf..38f8a81d3a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -21,7 +21,7 @@ A SimpleXről Kiemelőszín fogadott hívás - Hozzáférés a kiszolgálókhoz SOCKS proxyn a következő porton keresztül: %d? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. + Hozzáférés a kiszolgálókhoz SOCKS proxyn a következő porton keresztül: %d? A proxyt el kell indítani, mielőtt engedélyezné ezt a beállítást. Elfogadás Elfogadás gombra fent, majd: @@ -39,7 +39,7 @@ Előre beállított kiszolgálók hozzáadása A hívások kezdeményezése le van tiltva. Az összes partneréhez és csoporttaghoz külön TCP-kapcsolat (és SOCKS-hitelesítési adat) lesz használva.\nMegjegyzés: ha sok kapcsolata van, akkor az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet.]]> - hivatkozás előnézetének visszavonása + hivatkozáselőnézet visszavonása Az összes csevegési profiljához az alkalmazásban külön TCP-kapcsolat (és SOCKS-hitelesítési adat) lesz használva.]]> Mindkét fél küldhet eltűnő üzeneteket. Az Android Keystore-t a jelmondat biztonságos tárolására használják – lehetővé teszi az értesítési szolgáltatás működését. @@ -48,7 +48,7 @@ Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének küldése közvetlen kapcsolatot használ.]]> Alkalmazásadatok biztonsági mentése Az adatbázis előkészítése sikertelen - A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. + Az összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. A csevegési profillal (alapértelmezett), vagy a kapcsolattal (BÉTA). Egy új, véletlenszerű profil lesz megosztva. A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. @@ -59,7 +59,7 @@ Hang- és videóhívások Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével). Hívás fogadása - Az eltűnő üzenetek küldésének engedélyezése a partnerei számára. + Az eltűnő üzenetek küldése engedélyezve van a partnerei számára. Kapcsolódás folyamatban! Nem lehet fogadni a fájlt Hitelesítés elérhetetlen @@ -70,7 +70,7 @@ Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra) Továbbfejlesztett csoportok Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek. - Hívás befejeződött + A hívás véget ért HÍVÁSOK és további %d esemény Cím @@ -86,7 +86,7 @@ Vissza Kikapcsolható a beállításokban – az értesítések továbbra is meg lesznek jelenítve amíg az alkalmazás fut.]]> Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. - Hívások a zárolási képernyőn: + Hívások a zárolási képernyőn titkosítás elfogadása… Nem lehet meghívni a partnert! hibás az üzenet azonosítója @@ -97,7 +97,7 @@ Hozzáadás egy másik eszközhöz A reakciók hozzáadása az üzenetekhez engedélyezve van. Fájlelőnézet visszavonása - Az összes csoporttag kapcsolatban marad. + Az összes csoporttag továbbra is kapcsolatban marad. Több akkumulátort használ! Az alkalmazás mindig fut a háttérben – az értesítések azonnal megjelennek.]]> Letiltás adminisztrátor @@ -114,11 +114,11 @@ Az alkalmazásjelkód helyettesítve lesz egy önmegsemmisítő jelkóddal. Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek. Engedélyezi a hangüzeneteket? - Mindig használjon továbbítókiszolgálót + Mindig legyen használva továbbítókiszolgáló mindig - A hívás már befejeződött! + A hívás már véget ért! Engedélyezés - Az összes partnerével kapcsolatban marad. + Az összes partnerével továbbra is kapcsolatban marad. Élő csevegési üzenet visszavonása Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra) Hang- és videóhívások @@ -130,7 +130,7 @@ Megjelenés Az akkumulátor-optimalizálás aktív, ez kikapcsolja a háttérszolgáltatást és az új üzenetek időszakos lekérdezését. Ezt a beállításokban újraengedélyezheti. Letiltja a tagot? - %1$s hívása befejeződött + %1$s hívása véget ért Jó akkumulátoridő. Az alkalmazás 10 percenként ellenőrzi az új üzeneteket. Előfordulhat, hogy hívásokról, vagy a sürgős üzenetekről marad le.]]> szerző Az elküldött üzenetek végleges törlése engedélyezve van a partnerei számára. (24 óra) @@ -165,7 +165,7 @@ A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. Kiszolgáló hozzáadása Hang bekapcsolva - hanghívás (nem e2e titkosított) + hanghívás (végpontok között NEM titkosított) letiltva Módosítja az adatbázis jelmondatát? kapcsolódva @@ -195,11 +195,11 @@ Kapcsolódás partneri kapcsolatot kért kapcsolat %1$d - a partner e2e titkosítással rendelkezik + a partner végpontok közötti titkosítással rendelkezik Csoport létrehozása véletlenszerű profillal. A partner és az összes üzenet törölve lesz – ez a művelet nem vonható vissza! A partnerei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat. - Kapcsolódik az egyszer használható meghívóval? + Kapcsolódik az egyszer használható meghívón keresztül? Kapcsolódás egy hivatkozáson vagy QR-kódon keresztül Kapcsolódási hiba (AUTH) Csak név @@ -214,7 +214,7 @@ Kapcsolódik saját magához? Vágólapra másolva Kapcsolódási kérés elküldve! - Kapcsolódás a számítógéphez + Társítás számítógéppel Kapcsolat Helyesbíti a nevet a következőre: %s? Időtúllépés kapcsolódáskor @@ -224,7 +224,7 @@ Kapcsolat Kapcsolat megszakítva kapcsolat létrehozva - a partner nem rendelkezik e2e titkosítással + a partner nem rendelkezik végpontok közötti titkosítással Partner engedélyezi Rejtett név: Társítás számítógéppel @@ -266,25 +266,25 @@ kapcsolódás… Csevegési profil törlése egyéni - kapcsolódási hívás… + hívás kapcsolása… Téma személyre szabása - Jelenleg támogatott legnagyobb fájl méret: %1$s. + Jelenleg támogatott legnagyobb fájlméret: %1$s. Fájl törlése Hamarosan! cím módosítása %s számára… Csevegési adatbázis importálva Üzenetek törlése - Kiürítés + Ürítés Bezárás gomb A csevegés megállt (jelenlegi) Témák személyre szabása és megosztása. Törli a csevegési profilt? Titkos csoport létrehozása - Kapcsolódva a számítógéphez + Társítva a számítógéppel ICE-kiszolgálók beállítása Csoport törlése - Hitelesítés törlése + Ellenőrzés törlése készítő Megerősítés Csak nálam @@ -314,7 +314,7 @@ Egyéni időköz Kapcsolódás inkognitóban CSEVEGÉSEK - Új profil létrehozása a számítógép alkalmazásban. 💻 + Új profil létrehozása a számítógépes alkalmazásban. 💻 kapcsolódás (bejelentve) kapcsolódás… Csevegési adatbázis törölve @@ -333,7 +333,7 @@ Titkos csoport létrehozása Elvetés Törli a partnert? - Kiürítés + Ürítés Cím létrehozása, hogy az emberek kapcsolatba léphessenek Önnel. Biztonsági kódok összehasonlítása a partnerekével. Fájl-összehasonlítás @@ -341,9 +341,9 @@ Törli az üzenetet? Törli a függőben lévő kapcsolatot? Adatbázis titkosítva! - Kiüríti a csevegést? + Üríti a csevegés üzeneteit? Adatbázis visszafejlesztése - Üzenetek kiürítése + Csevegés üzeneteinek ürítése Az adatbázis titkosítási jelmondata frissítve lesz. Kapcsolódás automatikusan Adatbázishiba @@ -390,15 +390,15 @@ %2$s %1$d üzenetet moderált Eltűnő üzenet Ne hozzon létre címet - Ne mutasd újra + Ne jelenjen meg újra SimpleX-zár kikapcsolása - e2e titkosított + végpontok között titkosított ESZKÖZ - e2e titkosított videóhívás + végpontok között titkosított videóhívás közvetlen Számítógép %d perc - %d partner kijelölve + %d partner kiválasztva Engedélyezés %dhónap A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. @@ -419,7 +419,7 @@ Törlés, és a partner értesítése letiltva %d mp - Az összes fájl törlése + Összes fájl törlése Az adatbázis titkosítva lesz. Adatbázis-jelmondat és -exportálás Az adatbázis titkosítva lesz, a jelmondat pedig a Keystore-ban lesz tárolva. @@ -435,7 +435,7 @@ %d csoportesemény %d hónap Csoportprofil szerkesztése - e2e titkosított hanghívás + végpontok között titkosított hanghívás %d mp Decentralizált Dekódolási hiba @@ -443,12 +443,12 @@ Értesítések letiltása Eszközök Látható a helyi hálózaton - Ne engedélyezze + Nem engedélyezem Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben. alapértelmezett (%s) duplikált üzenet Leválasztja a számítógépet? - A számítógép-alkalmazás verziója (%s) nem kompatibilis ezzel az alkalmazással. + A számítógépes alkalmazás verziója (%s) nem kompatibilis ezzel az alkalmazással. Kézbesítés %d fájl, %s összméretben A csevegés megnyitásához adja meg az adatbázis jelmondatát. @@ -495,7 +495,7 @@ A csoportprofil a tagok eszközein tárolódik, nem a kiszolgálókon. Adja meg a jelmondatot… Hiba történt a felhasználói adatvédelem frissítésekor - Titkosít + Titkosítás Csoport nem található! Hiba történt az SMP-kiszolgálók mentésekor Visszafejlesztés és a csevegés megnyitása @@ -566,7 +566,7 @@ Hiba történt az XFTP-kiszolgálók mentésekor A tagok küldhetnek egymásnak közvetlen üzeneteket. Hiba történt a tag eltávolításakor - befejeződött + hívás vége A csoport üdvözlőüzenete Adja meg a csoport nevét: Hiba történt a meghívó elküldésekor @@ -631,7 +631,7 @@ Téves jelkód Azonnali Inkognitócsoportok - Hogyan + Útmutató Összecsukás Kép Továbbfejlesztett adatvédelem és biztonság @@ -695,7 +695,7 @@ moderált A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza! Győződjön meg arról, hogy a megadott XFTP-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva. - Nincs partner kijelölve + Nincs partner kiválasztva Nincsenek fogadott vagy küldött fájlok Megnyitás hordozható eszköz-alkalmazásban, majd koppintson a Kapcsolódás gombra az alkalmazásban.]]> Markdown az üzenetekben @@ -711,13 +711,13 @@ Helyi név Hálózat és kiszolgálók Értesítésekben megjelenő információk - Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗 + Társítsa össze a hordozható eszköz- és a számítógépes alkalmazásokat! 🔗 közvetett (%1$s) Hamarosan további fejlesztések érkeznek! A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben. Helytelen biztonsági kód! Ez akkor fordulhat elő, ha Ön vagy a partnere egy régi adatbázis biztonsági mentését használta. - Új számítógép-alkalmazás! + Új számítógépes alkalmazás! Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (megfigyelő szerepkör) meghívta őt: %1$s A reakciók hozzáadása az üzenetekhez le van tiltva. @@ -753,14 +753,14 @@ Az üzenetek végleges törlése le van tiltva. %s nevű hordozható eszköz le lett választva]]> hónap - Üzenetvázlat + Piszkozatok Egy üzenet eltüntetése Végleges üzenettörlés Egyszerre csak 10 videó küldhető el Csak Ön adhat hozzá reakciókat az üzenetekhez. elhagyta a csoportot Az üzenetek végleges törlése le van tiltva ebben a csevegésben. - Max 40 másodperc, azonnal fogadható. + Legfeljebb 40 másodperc, azonnal megérkezik. inkognitó a kapcsolattartási címhivatkozáson keresztül Onion kiszolgálók szükségesek a kapcsolódáshoz.\nMegjegyzés: .onion cím nélkül nem fog tudni kapcsolódni a kiszolgálókhoz. Olasz kezelőfelület @@ -777,7 +777,7 @@ Csak a csoport tulajdonosai engedélyezhetik a fájlok és a médiatartalmak küldését. Fájl betöltése… Nincs hozzáadandó partner - Üzenetvázlat + Piszkozatok függőben lévő kapcsolat Egyszer használható meghívó Értesítések @@ -818,7 +818,7 @@ Menük és figyelmeztetések Tagok meghívása Csatlakozás mint: %s - Nincs csevegés kijelölve + Nincs csevegés kiválasztva Csak helyi profiladatok inkognitó egy egyszer használható meghívón keresztül Moderálva: %s @@ -827,7 +827,7 @@ Beszélgessünk a SimpleX Chatben Moderálva Élő üzenetek - Hitelesítés + Megjelölés ellenőrzöttként Üzenetkézbesítési jelentések! hivatkozás előnézeti képe Elhagyja a csoportot? @@ -838,7 +838,7 @@ Új megjelenítendő név: Új jelmondat… nem fogadott hívás - Átköltöztetés: %s + Átköltöztetések: %s Válaszul erre Név és üzenet Az értesítések csak az alkalmazás bezárásáig érkeznek! @@ -851,7 +851,7 @@ dőlt Érvénytelen a fájl elérési útvonala Csatlakozik a csoporthoz? - nincs e2e titkosítás + nincs végpontok közötti titkosítás Új adatbázis-archívum Élő üzenet! Meghívás a csoportba @@ -872,7 +872,7 @@ Időszakos fogadott, tiltott Megismétli a kapcsolódási kérést? - Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra) + Csak Ön törölheti véglegesen az üzeneteket (partnere csak törlésre jelölheti meg azokat ). (24 óra) Szerepkör SimpleX kapcsolattartási cím Megállítás @@ -895,17 +895,17 @@ Jelentse a fejlesztőknek. Ön dönti el, hogy kivel beszélget. Az eltűnő üzenetek küldése le van tiltva. - Csak Ön tud hangüzeneteket küldeni. + Csak Ön küldhet hangüzeneteket. Frissítés Videó elküldve - Az adatbázis jelmondatának módosítása + Adatbázis jelmondatának módosítása Alkalmazásbeállítások megnyitása A jelkód nem módosult! Frissítés - Kijelölés - Csak Ön tud hívásokat indítani. + Kiválasztás + Csak Ön kezdeményezhet hívásokat. Biztonságos várólista - Értékelje az alkalmazást + Alkalmazás értékelése Egyszer használható meghívó megosztása Hiba történt az adatbázis visszaállításakor %s és %s @@ -918,7 +918,7 @@ Fogadott üzenet Üdvözlőüzenet %s, %s és további %d tag kapcsolódott - Csak a partnere tud hívást indítani. + Csak a partnere kezdeményezhet hívásokat. TÉMÁK Túl sok videó! Üdvözöljük! @@ -937,10 +937,10 @@ Hangszóró bekapcsolva Importált csevegési adatbázis használatához indítsa újra az alkalmazást. jogosulatlan küldés - Csak a partnere tud hangüzeneteket küldeni. + Csak a partnere küldhet hangüzeneteket. Beállítások A kapcsolódáshoz a partnere beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást. - visszaigazolás fogadása… + visszaigazolás érkezett… Biztonsági kód beolvasása a partnere alkalmazásából. Lépjen kapcsolatba a csoport adminisztrátorával. Videó bekapcsolva @@ -952,7 +952,7 @@ Keresés Újraegyezteti a titkosítást? Az önmegsemmisítő jelkód engedélyezve! - Biztonsági kiértékelés + Biztonsági felmérés Cím Üzenet elküldése Adatbázismentés visszaállítása @@ -1027,13 +1027,13 @@ SIMPLEX CHAT TÁMOGATÁSA SimpleX Chat szolgáltatás Ön megfigyelő - %s hitelesítve + %s ellenőrizve Jelszó a megjelenítéshez Adatvédelem és biztonság Eltávolítás A jelkód beállítva! Elküldött üzenet - Partnerek kijelölése + Partnerek kiválasztása ismeretlen üzenetformátum Kiszolgálók mentése Üdvözlőüzenet @@ -1041,7 +1041,7 @@ A profilfrissítés el lesz küldve a partnerei számára. Egyszerűsített inkognitómód Menti az üdvözlőüzenetet? - Új csevegési fiók létrehozásához indítsa újra az alkalmazást. + Új csevegési profil létrehozásához indítsa újra az alkalmazást. Engedély megtagadva! Függőben lévő hívás Adatbázis megnyitása… @@ -1049,7 +1049,7 @@ Jelmondat szükséges Privát értesítések Ön meghívta egy partnerét - %s nincs hitelesítve + %s nincs ellenőrizve Koppintson ide a kapcsolódáshoz Ennek az eszköznek a neve Jelenlegi profil @@ -1081,7 +1081,7 @@ Újraindítás SMP-kiszolgálók Videó - SimpleX-cím beállításainak mentése + SimpleX-címbeállítások mentése Újraegyeztetés Várakozás a videóra Saját XFTP-kiszolgálók @@ -1119,11 +1119,11 @@ Várakozás a képre Hangüzenetek Eltávolítja a tagot? - Biztonsági kód hitelesítése + Biztonsági kód ellenőrzése eltávolította Önt SimpleX-cím Megjelenítve: - válasz fogadása… + válasz érkezett… Visszaállítja az adatbázismentést? Üzenetek fogadása… %s és %s kapcsolódott @@ -1160,7 +1160,7 @@ Kihagyott üzenetek A hangüzenetek küldése le van tiltva. Partner nevének beállítása - Csak Ön tud eltűnő üzeneteket küldeni. + Csak Ön küldhet eltűnő üzeneteket. Médiatartalom megosztása… Ön: %1$s Beállítások @@ -1170,7 +1170,7 @@ A kapott hivatkozás beillesztése a partnerhez való kapcsolódáshoz… Beolvasás Port nyitása a tűzfalban - indítás… + hívás indítása… Leállítás elküldve SOCKS proxy használata @@ -1217,7 +1217,7 @@ Rendszer-hitelesítés Böngészőn keresztül Védje meg a csevegési profiljait egy jelszóval! - Csak a partnere tud eltűnő üzeneteket küldeni. + Csak a partnere küldhet eltűnő üzeneteket. Saját ICE-kiszolgálók QR-kód beolvasása a számítógépről SimpleX logó @@ -1239,7 +1239,7 @@ SimpleX-zár bekapcsolva elküldés a partnernek Beolvasás hordozható eszközről - Kapcsolatok hitelesítése + Kapcsolatok ellenőrzése Üzenet megosztása… másodperc A SimpleX-zár nincs bekapcsolva! @@ -1248,7 +1248,7 @@ Csevegési adatbázis eltávolította őt: %1$s Sikertelen kiszolgáló teszt! - Kapcsolat hitelesítése + Kapcsolat ellenőrzése Tudjon meg többet A fájl küldője visszavonta az átvitelt. Megállítja a csevegést? @@ -1256,7 +1256,7 @@ Beállítva 1 nap Felfedés Fogadott üzenetbuborék színe - Csak a partnere tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra) + Csak a partnere törölheti véglegesen az üzeneteket (Ön csak törlésre jelölheti meg azokat). (24 óra) Az önmegsemmisítő jelkód módosult! SimpleX Chat kiszolgálók használatban. SimpleX Chat kiszolgálók használata? @@ -1273,27 +1273,27 @@ Az üzenetváltás jövője Módosítja a hálózati beállításokat? Várakozás a hordozható eszköz társítására: - Biztonságos kapcsolat hitelesítése + Biztonságos kapcsolat ellenőrzése fájlok küldése egyelőre még nem támogatott Ön módosította a címet %s számára fájlok fogadása egyelőre még nem támogatott Csoportprofil mentése Visszaállítás alapértelmezettre Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. - videóhívás (nem e2e titkosított) + videóhívás (végpontok között NEM titkosított) Használat új kapcsolatokhoz - Az új üzeneteket az alkalmazás időszakosan lekéri – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ push-értesítéseket – az eszközről származó adatok nem lesznek elküldve a kiszolgálóknak. + Az új üzeneteket az alkalmazás időszakosan lekéri – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ leküldéses értesítéseket – az eszközről származó adatok nem lesznek elküldve a kiszolgálóknak. Számítógép címének beillesztése a kapcsolattartási címhivatkozáson keresztül - a SimpleX a háttérben fut a push értesítések használata helyett.]]> + a SimpleX a háttérben fut a leküldéses értesítések használata helyett.]]> A partnereinek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nVisszavonhatja ezt a kapcsolatot és eltávolíthatja a partnert (ezt később ismét megpróbálhatja egy új hivatkozással). A jelmondat nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonsági mentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. - A partnerei továbbra is kapcsolódva maradnak. + A partnereivel továbbra is kapcsolatban marad. A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze a jelszavát. Az adatbázis nem működik megfelelően. Koppintson ide a további információkért A fájl küldése le fog állni. Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál. - Nem sikerült hitelesíteni; próbálja meg újra. + Nem sikerült ellenőrizni; próbálja meg újra. Az üzenet az összes tag számára moderáltként lesz megjelölve. Értesítések fogadásához adja meg az adatbázis jelmondatát A teszt a(z) %s lépésnél sikertelen volt. @@ -1329,14 +1329,14 @@ %1$s.]]> Profil felfedése Ez nem egy érvényes kapcsolattartási hivatkozás! - A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal. + A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal. A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi partnerétől. Ez a beállítás csak az Ön jelenlegi csevegési profiljában lévő üzenetekre vonatkozik Ön meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. Ez a csoport már nem létezik. A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül. Ön meghívást kapott a csoportba - A partnere a jelenleg megengedett maximális méretű (%1$s) fájlnál nagyobbat küldött. + A partnere a jelenleg támogatott legnagyobb (%1$s) fájlméretnél nagyobbat küldött. A partnerei és az üzenetek (kézbesítés után) nem a SimpleX kiszolgálókon vannak tárolva. Üzenetek formázása a szövegbe szúrt speciális karakterekkel: Megnyitás az alkalmazásban gombra.]]> @@ -1348,14 +1348,14 @@ Átvitelelkülönítés Akkor lesz kapcsolódva, ha a kapcsolódási kérését elfogadják, várjon, vagy ellenőrizze később! A hangüzenetek küldése le van tiltva. - Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> + Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> Biztonságos kvantumbiztos protokollon keresztül. - legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni időkorlát beállítása az üzenetek eltűnéséhez.\n- előzmények szerkesztése. Társítás számítógéppel menüt a hordozható eszköz alkalmazásban és olvassa be a QR-kódot.]]> %s ekkor: %s Akkor lesz kapcsolódva, amikor a partnerének az eszköze online lesz, várjon, vagy ellenőrizze később! Kéretlen üzenetek elrejtése. - Onion kiszolgálók használata opciót „Nemre”, ha a SOCKS proxy nem támogatja őket.]]> + Onion kiszolgálók használata beállítást „Nemre”, ha a SOCKS proxy nem támogatja őket.]]> Megoszthatja a címét egy hivatkozásként vagy egy QR-kódként – így bárki kapcsolódhat Önhöz. Létrehozás később A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX kiszolgálók nem láthatják a profilját. @@ -1366,7 +1366,7 @@ Csoportmeghívó elküldve Frissíti az átvitelelkülönítési módot? Átvitelelkülönítés - Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak. + Nem fog több üzenetet kapni ebből a csoportból, de a csevegés előzményei megmaradnak. A csevegési adatbázis nem titkosított – állítson be egy jelmondatot annak védelméhez. Közvetlen internetkapcsolat használata? Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. @@ -1389,8 +1389,8 @@ A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár. kapcsolatba akar lépni Önnel! Ön a következőre módosította a saját szerepkörét: „%s” - A csevegési szolgáltatás elindítható a „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával. - Kód hitelesítése a hordozható eszközön + A csevegés elindítható az alkalmazás „Beállítások / Adatbázis” menüjében vagy az alkalmazás újraindításával. + Kód ellenőrzése a hordozható eszközön Ön csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet a friss hírekről.]]> Nem kötelező üdvözlőüzenettel. @@ -1402,7 +1402,7 @@ %1$s nevű csoporthoz!]]> A hangüzenetek küldése le van tiltva ebben a csevegésben. Ön irányítja csevegését! - Kód hitelesítése a számítógépen + Kód ellenőrzése a számítógépen Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak. A csatlakozási kérése el lesz küldve ennek a csoporttagnak. Ha egy inkognitóprofilt oszt meg valamelyik partnerével, a rendszer ezt az inkognitóprofilt fogja használni azokban a csoportokban, ahová az adott partnere meghívja Önt. @@ -1414,12 +1414,12 @@ A kézbesítési jelentések küldése az összes partnere számára engedélyezve lesz. Protokoll időtúllépése kB-onként Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be. - Ez a művelet nem vonható vissza – a kijelöltnél korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet. + Ez a művelet nem vonható vissza – a kiválasztott üzenettől korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet. A profilja csak a partnereivel van megosztva. Néhány kiszolgáló megbukott a teszten: Koppintson ide a csatlakozáshoz Ez a művelet nem vonható vissza – az összes fogadott és küldött fájl a médiatartalmakkal együtt törölve lesznek. Az alacsony felbontású képek viszont megmaradnak. - A kézbesítési jelentések engedélyezve vannak %d partnernél + A kézbesítési jelentések engedélyezve vannak %d partner számára Küldés a következőn keresztül: Köszönet a felhasználóknak a Weblate-en való közreműködésért! A kézbesítési jelentések küldése engedélyezve lesz az összes látható csevegési profilban lévő összes partnere számára. @@ -1443,7 +1443,7 @@ Köszönet a felhasználóknak a Weblate-en való közreműködésért! Jelmondat mentése a beállításokban Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem lesznek elküldve. - A második jelölés, amit kihagytunk! ✅ + A második pipa, ami már nagyon hiányzott! ✅ A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók el lesznek távolítva. @@ -1452,10 +1452,10 @@ Profil és kiszolgálókapcsolatok Egy üzenetváltó- és alkalmazásplatform, amely védi az adatait és biztonságát. Koppintson ide a profil aktiválásához. - A kézbesítési jelentések le vannak tiltva %d partnernél - Munkamenet kód + A kézbesítési jelentések le vannak tiltva %d partner számára + Munkamenet kódja Köszönet a felhasználóknak a Weblate-en való közreműködésért! - Kis csoportok (max. 20 tag) + Kis csoportok (legfeljebb 20 tag) Az Ön által elfogadott kapcsolat vissza lesz vonva! Élő üzenet küldése – az üzenet a címzett(ek) számára valós időben frissül, ahogy Ön beírja az üzenetet A KÉZBESÍTÉSI JELENTÉSEKET A KÖVETKEZŐ CÍMRE KELL KÜLDENI @@ -1519,7 +1519,7 @@ Számítógép elfoglalt Számítógép inaktív Csevegés újraindítása - Időtúllépés a számítógéphez való csatlakozáskor + Időtúllépés a számítógéphez való társításkor A számítógép le lett választva A kapcsolat megszakadt A kapcsolat megszakadt @@ -1555,7 +1555,7 @@ Privát jegyzetek Hiba történt a privát jegyzetek törlésekor Hiba történt az üzenet létrehozásakor - Kiüríti a privát jegyzeteket? + Üríti a privát jegyzetek tartalmát? Létrehozva Mentett üzenet Létrehozva: %s @@ -1586,7 +1586,7 @@ Az üdvözlőüzenet túl hosszú Az adatbázis átköltöztetése folyamatban van.\nEz eltarthat néhány percig. Hanghívás - A hívás befejeződött + Hívás vége Videóhívás Hiba történt a böngésző megnyitásakor A hívásokhoz egy alapértelmezett webböngésző szükséges. Állítson be egy alapértelmezett webböngészőt az eszközön, és osszon meg további információkat a SimpleX Chat fejlesztőivel. @@ -1613,14 +1613,14 @@ Hiba történt a beállítások mentésekor Hiba történt az archívum letöltésekor Hiba történt az archívum feltöltésekor - Hiba történt a jelmondat hitelesítésekor: + Hiba történt a jelmondat ellenőrzésekor: Az exportált fájl nem létezik A fájl törölve lett, vagy érvénytelen a hivatkozás %s letöltve Archívum importálása Feltöltés előkészítése - Az adatbázis jelmondatának hitelesítése - Jelmondat hitelesítése + Adatbázis jelmondatának ellenőrzése + Jelmondat ellenőrzése Jelmondat beállítása Kép a képben hívások Biztonságosabb csoportok @@ -1633,10 +1633,10 @@ A folytatáshoz a csevegést meg kell szakítani. Csevegés megállítása folyamatban Vagy ossza meg biztonságosan ezt a fájlhivatkozást - Csevegés indítása + Csevegés elindítása Nem szabad ugyanazt az adatbázist használni egyszerre két eszközön.]]> Az átköltöztetéshez erősítse meg, hogy emlékszik az adatbázis jelmondatára. - Átköltöztetés egy másik eszközről opciót az új eszközén és olvassa be a QR-kódot.]]> + Átköltöztetés egy másik eszközről beállítást az új eszközén és olvassa be a QR-kódot.]]> Átköltöztetés véglegesítése Átköltöztetés véglegesítése egy másik eszközön. Letöltés előkészítése @@ -1739,10 +1739,10 @@ Nem Nem védett Igen - NE használjon privát útválasztást. + NE legyen használva privát útválasztás. Privát útválasztás Privát útválasztás használata az ismeretlen kiszolgálókhoz. - Mindig használjon privát útválasztást. + Mindig legyen használva privát útválasztás. Üzenet-útválasztási mód Közvetlen üzenetküldés, ha az IP-cím védett és a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. Közvetlen üzenetküldés, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. @@ -1816,7 +1816,7 @@ Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén. Ellenőrizze, hogy a hordozható eszköz és a számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint a számítógép tűzfalában engedélyezve van-e a kapcsolat.\nMinden további problémát osszon meg a fejlesztőkkel. Nem lehet üzenetet küldeni - A kijelölt csevegési beállítások tiltják ezt az üzenetet. + A kiválasztott csevegési beállítások tiltják ezt az üzenetet. Próbálja meg később. A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. Inaktív tag @@ -1843,7 +1843,7 @@ Újrakapcsolódás az összes kiszolgálóhoz Hiba történt a statisztikák visszaállításakor Visszaállítás - Az összes statisztika visszaállítása + Összes statisztika visszaállítása Visszaállítja az összes statisztikát? A kiszolgálók statisztikái visszaállnak – ez a művelet nem vonható vissza! Részletes statisztikák @@ -1976,12 +1976,12 @@ Engedélyeznie kell a hívásokat a partnere számára, hogy fel tudják hívni egymást. A(z) %1$s nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. Üzenet… - Kijelölés + Kiválasztás Az üzenetek az összes tag számára moderáltként lesznek megjelölve. - Nincs semmi kijelölve + Nincs semmi kiválasztva Az üzenetek törlésre lesznek jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. Törli a tagok %d üzenetét? - %d kijelölve + %d kiválasztva Az üzenetek az összes tag számára törölve lesznek. Csevegési adatbázis exportálva Kapcsolatok- és kiszolgálók állapotának megjelenítése. @@ -2024,7 +2024,7 @@ CSEVEGÉSI ADATBÁZIS Profil megosztása Rendszerbeállítások használata - Csevegési profil kijelölése + Csevegési profil kiválasztása Ne használja a hitelesítési adatokat proxyval. Különböző proxy-hitelesítési adatok használata az összes profilhoz. Különböző proxy-hitelesítési adatok használata az összes kapcsolathoz. @@ -2048,7 +2048,7 @@ %1$s üzenet nem lett továbbítva Továbbít %1$s üzenetet? Továbbítja az üzeneteket fájlok nélkül? - Az üzeneteket törölték miután kijelölte őket. + Az üzeneteket törölték miután kiválasztotta őket. %1$s üzenet mentése Hiba történt az üzenetek továbbításakor Hang elnémítva @@ -2060,8 +2060,8 @@ Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítési adatok lesznek használva. Alkalmazás munkamenete Az összes kiszolgálóhoz új, SOCKS-hitelesítési adatok lesznek használva. - Kattintson a címmező melletti info gombra a mikrofon használatának engedélyezéséhez. - Nyissa meg a Safari Beállítások / Weboldalak / Mikrofon menüt, majd válassza a helyi kiszolgálók engedélyezése lehetőséget. + Kattintson a címmező melletti információ gombra a mikrofon használatának engedélyezéséhez. + Nyissa meg a Safari / Beállítások / Weboldalak / Mikrofon menüt, majd válassza a helyi kiszolgálók engedélyezése beállítást. Hívások kezdeményezéséhez engedélyezze a mikrofon használatát. Fejezze be a hívást, és próbálja meg a hívást újra. Továbbfejlesztett hívásélmény Továbbfejlesztett üzenetdátumok. @@ -2178,7 +2178,7 @@ Értesítések és akkumulátor Az alkalmazás mindig fut a háttérben Elhagyja a csevegést? - Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. + Nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. Csevegés törlése Meghívás a csevegésbe Barátok hozzáadása @@ -2236,7 +2236,7 @@ Nincsenek olvasatlan csevegések Lista létrehozása Lista mentése - Az összes csevegés el lesz távolítva a következő listáról, és a lista is törlődik: %s + Az összes csevegés el lesz távolítva a(z) %s nevű listáról, és a lista is törölve lesz Törlés Törli a listát? Szerkesztés @@ -2293,7 +2293,7 @@ alapértelmezett (%s) Csevegési üzenetek törlése az eszközről. Módosítja az automatikus üzenettörlést? - Ez a művelet nem vonható vissza – a kijelölt üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből. + Ez a művelet nem vonható vissza – a kiválasztott üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből. A következő TCP-port használata, amikor nincs port megadva: %1$s. TCP-port az üzenetváltáshoz Webport használata @@ -2301,7 +2301,7 @@ Összes némítása Legfeljebb %1$s tagot említhet meg egy üzenetben! Az üzenetek jelentése a moderátorok felé engedélyezve van. - Az üzenetek a moderátorok felé történő jelentésének megtiltása. + Az üzenetek jelentése a moderátorok felé le van tiltva. Archiválja az összes jelentést? Archivál %d jelentést? Csak magamnak @@ -2393,7 +2393,7 @@ csatlakozási kérés elutasítva Ön elhagyta a csoportot a tag régi verziót használ - Hiba a csevegés törlésekor + Hiba történt a csevegés törlésekor Ön nem tud üzeneteket küldeni! a partner nem áll készen nincs szinkronizálva @@ -2450,9 +2450,9 @@ TCP-kapcsolat időtúllépése a háttérben Profil betöltése… Rövid leírás: - Saját névjegy: - Névjegy: - A névjegy túl hosszú + Saját életrajz: + Életrajz: + Az életrajz túl hosszú A leírás túl hosszú Partneri kapcsolatkérés elfogadása Üzleti kapcsolat @@ -2468,7 +2468,7 @@ Saját cím létrehozása Eltűnő üzenetek engedélyezése alapértelmezetten. Tartsa tisztán a csevegéseit - Névjegy és üdvözlőüzenet beállítása a profilokhoz. + Életrajz és üdvözlőüzenet beállítása a profilokhoz. Saját cím megosztása Rövid SimpleX-cím Cím frissítése @@ -2503,6 +2503,23 @@ A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. - nincs előfizetés - Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs előfizetés). + nincs feliratkozás + Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs feliratkozás). + Tag üzeneteinek törlése + Törli a tag üzeneteit? + Üzenetek törlése + A tag üzenetei törölve lesznek – ez a művelet nem vonható vissza! + Eltávolítás és az üzeneteinek törlése + Összes üzenet + Fájlok + Szűrő + Képek + Hivatkozások + Fájlok keresése + Képek keresése + Hivatkozások keresése + Videók keresése + Hangüzenetek keresése + Videók + Hangüzenetek diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image_filled.svg new file mode 100644 index 0000000000..045484d0a1 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image_filled.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_library.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_library.svg new file mode 100644 index 0000000000..091b0d4692 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_library.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_library_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_library_filled.svg new file mode 100644 index 0000000000..72692b2e17 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_library_filled.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 909c6c7cfe..2257d93efa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -2509,4 +2509,9 @@ Sidik jari di alamat server tidak cocok dengan sertifikat: %1$s. tidak berlangganan Anda tidak terhubung ke server yang digunakan untuk menerima pesan dari koneksi ini (tidak berlangganan). + Hapus pesan anggota + Hapus pesan anggota? + Hapus pesan + Pesan anggota akan dihapus - ini tidak dapat dibatalkan! + Hapus pesan diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 1c7e39d51e..1c191a78bd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -2452,7 +2452,7 @@ Entra nel gruppo Apri la chat Apri una chat nuova - Apri un gruppo nuovo + Apri il nuovo gruppo Apri per accettare Apri per connettere Apri per entrare @@ -2541,4 +2541,21 @@ L\'impronta digitale nell\'indirizzo del server non corrisponde al certificato: %1$s. nessuna iscrizione Non sei connesso/a al server usato per ricevere messaggi da questa connessione (nessuna iscrizione). + Elimina i messaggi del membro + Eliminare i messaggi del membro? + Elimina i messaggi + I messaggi del membro verranno eliminati. Non è reversibile! + Rimuovi ed elimina i messaggi + Tutti i messaggi + File + Immagini + Link + Cerca file + Cerca immagini + Cerca link + Cerca video + Cerca messaggi vocali + Video + Messaggi vocali + Filtro diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index 6b413c9bfa..fb83b83735 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -2138,4 +2138,17 @@ פתח שיחה חדשה פתח קבוצה חדשה שלח את המשוב הפרטי שלך לקבוצות. + הסכמה לבקשת חבר + הערות + לא נבחר כלום להעברה! + התראות וסוללה + הוסף הודעה + אפשר קבצים ומדיה רק כאשר החבר מאשר אותם + אפשר לאנשי קשר שלך לשלוח קבצים ומדיה + אודות: + האודות ארוך מדי + בוט + אתה והאיש קשר שלך יכולים לשלוח קבצים ומדיה + צ\'אט עסקי + אי אפשר לשנות תמונת פרופיל diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index cc85e49a8c..1c4d265515 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -347,7 +347,7 @@ データベースパスフレーズ データベースをエクスポート データベースを削除 - データベースを読み込みますか? + データベースのインポート 新しいデータベースのアーカイブ 過去のデータベースアーカイブ ファイルを全て削除 @@ -2042,4 +2042,17 @@ プライベートメッセージルーティング用のサーバーがありません。 メディアおよびファイルサーバーは存在しません。 ファイルを送信するサーバーがありません。 + ソーシャルメディア向け + サーバを利用する + あなたのサーバ + ビデオ + ファイル + 画像 + リンク + すべて + 音声メッセージ + フィルター + メンバーとして承認する + オブザーバーとして承認する + スパム diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml new file mode 100644 index 0000000000..0ea9328085 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml @@ -0,0 +1,837 @@ + + + Profîla niha bişuxulîne + Komê veke + Komeke nû veke + Lînka xelet + Databas tê vekirin… + xeletî + Profîl nikarîbû were çêkirin! + Profîl nikarîbû were guhertin! + Ti serverên medya & dosyayan nînin. + Ji min re + Ji hemû moderatoran re + Xeletî: %1$s + Cewab bide + Kopî bike + Qeyd bike + Biguhere + Melûmat + Lê bigere + Li sûretan bigere + Li vîdyoyan bigere + Li dosyayan bigere + Li lînkan bigere + Sûret + Vîdyo + Dosya + Lînk + Tarîx + Tarîx nîne + Cewab ji bo + Qeydkirî + Hatiye qeydkirin ji + Jê bibe + Veşêre + Bihêle + Gilî bike + Hilbijêre + Mezin bike + Şandina dosyayê bisekinîne? + Şandina dosyayê wê bê sekinandin. + Standina dosyayê bisekinîne? + Bisekinîne + Dosya wê ji serveran bê jêbirin. + Daxe + Lîste + Endam ne aktîv e + guhertî + şandî + şandin bi ser neket + nexwendî + Bi xêr hatî %1$s! + Bi xêr hatî! + Ev nivîs di eyaran de heye + Eyar + Bi navê %s bikeviyê + redkirî + Hemû + Lîste lê zêde bike + 1 gilîkirin + %d gilîkirin + Gilîkirinên endaman + Zêde sûret hene! + Zêde vîdyo hene! + Tenê 10 sûret karin di derbekê de werin şandin + Tenê 10 vîdyo karin di derbekê de werin şandin + Xeletiya dekodkirinê + Sûret nikare were dekodkirin. Bi xêra xwe, sûretekî dî biceribîne yan jî xeberê bide mielifan. + Dosya û medya memnû in! + Tenê xwediyên koman karin dosya û medya aktîv bikin. + Lînkên SimpleXê memnû in + Xwestinê bişîne + xwestin şandî ye + ne sinkronîzekirî ye + Bi xêra xwe xeberê bide admînê komê. + kom jêbirî ye + ji komê derxistî + tu derketî + Sûret + Li hêviya sûret e + Sûret hat şandin + Li hêviya sûret e + Vîdyo + Li hêviya vîdyo ye + Vîdyo hat şandin + Li hêviya vîdyoyê ye + Dosya + Koda emniyetê tesdîq bike + Kamera + Ji Galeriyê + Dosya + Dosyakê hilbijêre + Sûret + Vîdyo + Pêl pişkokê bike + li ser, piştre: + Komekê çêke: ji bo çêkirina komeke nû.]]> + Qebûl bike + Red bike + Heya kesê ko şandiye jê çênabe. + Endam hatiye jêbirin - nikare xwestinê qebûl bike + Jê bibe + Jê bibe + Xwendî nîşan bide + Nexwendî nîşan bide + Bêdeng bike + Hemûka bêdeng bike + Bêdengkirinê betal bike + Bike favorît + Ji favorîtan derxe + Behsên nexwendî + Lîste çêke + Li lîstê zêde bike + Lîstê biguhere + Lîstê qeyd bike + Navê lîstê... + Navê lîstê û emojiya wê divê ji bo her lîsteyî cuda be. + Jê bibe + Lîstê jê bibe? + Biguhere + Rêzê biguhere + sûretê profîlê + Pişkoka girtinê + Eyar + Koda QRyê + Adresa SimpleXê + arîkarî + Taximê SimpleXê + Logoya SimpleXê + E-poste + Bêhtir + Koda QRyê nîşan bide + Lînka 1-carê bi hevalekî re parve bike + Ji bo ko tu xwe ji guhertina lînka biparêzî, tu karî kodên emniyetê yên kontaktê qiyas bikî. + Yan jî vê kodê nîşan bide + Lînka timam + Lînka kurt + Profîlê parve bike + Profîl nikarîbû were guhertin + Yan jî koda QRyê skan bike + Dewetiya neşuxulandî bihêle? + Bihêle + Lînk tê çêkirin… + Dîsa biceribîne + Vê lînka 1-carê parve bike + Lînka ko te standiye bizeliqîne + Nivîsa ko te zeliqand ne lînkeke SimpleXê ye. + Pêl vir bike ji bo zeliqandina lînkê + Profîla te + Profîl nikare were guhertin + Kod skan bike + Koda emniyetê xelet e! + Koda emniyetê + Tesdîqkirî nîşan bide + Tesdîqkirinê jê bibe + %s tesdîqkirî ye + %s ne tesdîqkirî ye + Eyarên te + Adresa te yî SimpleXê + Li ser SimpleX Chat + Çawa tê şuxulandin + Arîkariya Markdownê + Pirsan û fikran bişîne + E-poste ji me re bişîne + Server bi kar bîne + Serverên SimpleX Chat tên şuxulandin. + Çilo + Çilo yek serverên xwe dişuxulîne + Mecbûrî + Neparastî + Ne ti carî + Erê + Wextê ko IP veşartî ye + Na + Girtî + Saxlem + Beta + %s (%s) daxe + Cihê dosyayê veke + Dûvre bîne bîra min + Adresê jê bibe? + Lînkê parve bike + Adresa SimpleXê çêke + Hew adresê parve bike? + Hew parve bike + Qebûlkirina ji ber xwe ve + Eyaran qeyd bike? + Eyarên adresa SimpleXê qeyd bike + Adresê jê bibe + Hevalan dewet bike + Em li SimpleX Chat qise bikin + Ji bo medyaya sosyal + Yan ji bo parvekirina şexsî + Adresa SimpleXê yan jî lînka 1-carê? + Lînka 1-carê çêke + Eyarên adresê + Sûret jê bibe + Tercihan qeyd bike? + Bê qeydkirinê derkeve + Profîlê veşêre + Şîfra profîlê qeyd bike + Li ser SimpleXê + Markdown çawa tê şuxulandin + qalin + xwehr + a + b + bi reng + tê telefonkirin… + Kamera + Kamera û mîkrofon + Van destûran bide ji bo telefonkirinê + Di eyaran de destûrê bide + Vê destûrê di eyarên Androidê de bibîne û bixwe destûrê bide. + Eyaran veke + Bluetooth + Profîla xwe çêke + Çawa dişuxule + SimpleX çawa dişuxule + Çawa tesîrê li pîlê dike + Her serê pêlekê + Di cih de + Notîfîkasyon û pîl + Tu karî serveran ji eyaran eyar bikî. + Dewam bike + Qebûl bike + Red bike + Qebûl bike + Nîşan bide + Eyar nikarîbû were guhertin + Dewam bike + Şîfrê ji eyaran bibe? + Jê bibe + Şîfra niha… + Şîfra nû… + Endaman dewet bike + Çêtir tecrûba karber + Adresa xwe parve bike + saniye + deqe + seet + roj + heftî + heyv + Hilbijêre + Telefonekê girê de + Telefonên girêdayî + Ji telefonê skan bike + Navê vê cihazê + (ev cihaz v%s)]]> + Telefona girêdayî + Girêdayî telefonê + Navê vê cihazê binivîsîne… + Xeletî + Ev cihaz + Cihaz + Cihaza mobîlê yî nû + %s hat qutkirin]]> + Girêdan sekinî + Girêdan sekinî + Hemû statîstîkan vala bike + Serveran qeyd bike? + Skan bike / Lînk bizeliqîne + Koda QRyê skan bike + Ji kompîterê koda QRyê skan bike + Koda QRyê ya serverê skan bike + %s (niha) + %s daxistî + lê bigere + Lê bigere yan jî lînka SimpleXê bizeliqîne + san + Ê diwan + Dora emîn + koda emniyetê hat guhertin + Xeletiyên şandinê + şandina dosyayan hê ne mimkun e + Tê şandin bi riya + Pêşdîtinên lînkan bişîne + Gilîkirinên şexsî bişîne + Wextê şandinê: + Cewaba şandî + Bi riya proksiyê şandî + Server + Adresa serverê + Adres + Adresa serverê li eyarên torê nayê. + SERVER + Melûmata serveran + Ceribandina serverê bi ser neket! + Versiyona serverê li eyarên torê nayê. + 1 roj deyne + Tercihên komê diyar bike + EYAR + Parve bike + Lînka 1-carê parve bike + Adresê parve bike + Adresê bi hişkereyî parve bike + Dosya parve bike… + Medya parve bike… + Adresa kevn parve bike + Lînka kevn parve bike + Adresa SimpleXê li medayaya sosyal parve bike. + Behsa kin: + Adresa SimpleXê yî kin + Nîşan bide: + Pêşdîtinê nîşan bide + Bigire + Bigire? + SimpleX + Adresa SimpleXê + Lînka qenala SimpleXê + Xizmeta SimpleX Chatê + Lînka komê ya SimpleXê + Lînkên SimpleXê + Lînkên SimpleXê + Lînkên SimpleXê memnû in. + simplexmq: v%s (%2s) + Dewetiya yek carê ya SimpleXê + Mezinbûnî + Ser bakirina endaman ve derbas bibe + Funksiyona hêdî + Komên piçûk (herî zêde 20) + Servera SMPyê + Serverên SMPyê + Nerm + Bêdeng + Spam + Spam + Çarçik, girover, yan çi tiştê di neqebê de. + %s: %s + %s saniye + %s server + Li GitHubê stêrkê bide + dest pê dike… + Her serê pêlekê dest pê dike + Statîstîk + Bisekinîne + Dosyayê bisekinîne + xet/xêz/xîşk + Biqewet + Abonekirî + PIŞT BIDE SIMPLEX CHATÊ + Biguhere + Sîstem + Sîstem + Sîstem + Sîstem + Moda sîstemê + Terî/Dûvik + Pêl Adresa SimpleXê çêke di meniwê de ji bo ko tu dûvre çêkî. + Pêl Bikeve komê bike + Bikeviyê + Bikeve komê + Bikeve komê? + Bikeve komê? + Dikeve komê + Bikeve koma xwe? + %1$d dosya hê tê(n) daxistin. + %1$d dosya nikarîbû(n) wer(e/in) daxistin. + %1$d dosya hat(in) jêbirin. + %1$d dosya nehat(in) daxistin. + %1$d xeletiyên dosyayê ên dî. + %1$s ENDAM + 1 roj + 1 deqe + 1 heyv + lînka 1-carê + 1 heftî + 1 sal + 30 saniye + 5 deqe + Betal bike + Guhertina adresê betal bike + Guhertina adresê betal bike? + Li ser adresa SimpleXê + Qebûl bike + Qebûl bike + Qebûl bike + Wek endamekî qebûl bike + Şertan qebûl bike + %1$s hat qebûlkirin + Şertên qebûlkirî + dewetiyê qebûl kir + tu qebûl kirî + Endam qebûl bike + Girêdanên aktîv + Hevalan lê zêde bike + Guhertina adresê wê bê betalkirin. Adresa berê yî standinê wê bê şuxulandin. + Adres yan jî lînka 1-carê? + Serverekê lê zêde bike + Bi riya skankirina kodên QRyê serveran lê zêde bike. + Endamên têxim lê zêde bike + Cihazeke dî lê zêde bike + admîn + admîn + Admîn karin endamekî ji bo her kesî blok bikin. + Admîn karin lînkên lêzêdebûna koman çêkin. + Eyarên torê ên pêşketî + Eyarên pêşketî + Eyarên pêşketî + Hinek tiştên dî + hemû + Hemû dataya aplîkasyonê hat jêbirin. + hemû endam + Bihêle + Bihêle ko dosya û medya werin şandin. + Bihêle ko lînkên SimpleXê werin şandin. + Hemû profîl + Hemû server + Jixwe tê girêdan! + Jixwe dikeve komê! + hercar + Hercar + Hercar vekirî + û %d hewadîsên dî + Biguhere + Şîfra databasê biguhere? + rola %s hat guhertin %s + rola te hat guhertin %s + Rola komê biguhere? + Adresa standinê biguhere + Adresa standinê biguhere? + Rolê biguhere + adres tê guhertin… + adres tê guhertin… + adresa %s tê guhertin… + %1$d xeletiyên dosyayan:\n%2$s + %1$s dixwaze bi te re bikeve danûstandinê bi riya + Adresa serverê kontrol bike û dîsa biceribîne. + Girêdana xwe yî înternetê kontrol bike û dîsa biceribîne + Notên şexsî vala bike? + Pêl pişkoka melûmatê ya nêzîkî cihê adresê bike ji bo destûrdana mîkrofonê. + Moda reng + Di wextekî nêzîk de tê! + Dosya qiyas bike + timam + Timam bûye + Vala bike + Vala bike + Vala bike + Şert di %s de hatin qebûlkirin. + Şertên şuxulandinê + Şert wê di %s de bên qebûlkirin. + Serverên SMPyê ên eyarkirî + Serverên XFTPyê ên eyarkirî + Serverên ICEyê eyar bike + Dosyayên ji serverên nenas qebûl bike. + Eyarên torê tesdîq bike. + Şîfra nû dîsa binivîsîne… + Bi xwe re bikeve danûstandinê? + Bi riya lînkê bikeve danûstandinê + Bi riya lînkê bikeve danûstandinê? + Bi riya lînkê / koda QRyê bikeve danûstandinê + Bi riya lînka yek carê bikeve danûstandinê? + Bi %1$s re bikeve danûstandinê? + Muhtewa ne li gora şertên şuxulandinê ye + Îkona kontekstê + Dewam bike + Beşdar bibe + Tora xwe kontrol bike + Xeletiyê kopî bike + Çêke + Çêke + Adres çêke + Adresekê çêke ji bo ko xelk karibin bi te re bikevin danûstandinê. + Hat çêkirin + Wextê çêkirinê + Wextê çêkirinê: %s + Dosya çêke + Kom çêke + Lînka komê çêke + Lînk çêke + Lînkeke dewetiyê ya yek carî çêke + Profîl çêke + Profîl çêke + Dor çêke + Komeke veşartî çêke + Komeke veşartî çêke + Adresa xwe çêke + Lînka arşîvê tê çêkirin + kesê ko çêkiriye + Xeletiya cidî + (niha) + Profîla niha + Tarî + Tarî + Moda tarî + Rengên moda tarî + Xuyakirina tarî + IDya databasê + IDya databasê: %d + %dr + %d roj + %d roj + jiberxweve (%s) + jiberxweve (%s) + Jê bibe piştî + Hemû dosyayan jê bibe + Jêbirî + Wextê jêbirinê + Wextê jêbirinê: %s + Dosya jê bibe + Ji bo min jê bibe + Komê jê bibe + Komê jê bibe? + Lînkê jê bibe + Lînkê jê bibe? + Profîlê jê bibe + Dorê jê bibe + Serverê jê bibe + Xeletiyên jêbirinê + Gihan/Gihiştin + Cihazên kompîter + Kompîter mijûl e + Kompîter ne aktîv e + Girêdana bi kompîterê re qut bû + Detay + CIHAZ + %d dosya bi mezibnbûniya timam ya %s + %d hewadîsên komê + %d seet + %d seet + Bigire + girtî + girtî + Ji bo her kesî bigire + Ji bo hemû koman bigire + %d deqe + %d deqe + %d heyv + %d heyv + %d heyv + Adres çêneke + Dîsa nîşan nede + Daxe + Daxistî + Dosyayên daxistî + Xeletiyên daxistinê + Daxistin bi ser neket + Dosya daxe + Detayên lînkê tên daxistin + %d heftî + Profîla komê biguhere + Sûret biguhere + Veke + vekirî + Vekirî heta + ji te re vekirî + Ji bo her kesî veke + Ji bo hemû koman veke + xilasbûyî + Navê komê binivîsîne: + Şîfra rast binivîsîne. + Şîfrê binivîsîne + Şîfrê binivîsîne… + Di lêgeranê de şîfrê binivîsîne + Navê xwe binivîsîne: + Xeletî + Xeletî + Xeletî + Xeletî di betalkirina guhertina adresê de + Xeletî di qebûlkirina şertan de + Xeletî di qebûlkirina xwestina ketina danûstandinê de + Xeletî di qebûlkirina endêm de + Xeletî di lêzêdekirina endam(an) de + Xeletî di lêzêdekirina serverê de + Xeletî di guhertina adresê de + Xeletî di guhertina profîlê de + Xeletî di guhertina rolê de + Xeletî di çêkirina adresê de + Serverên te yên XFTPyê + Te dewetîke komê şand + Serverên te yên SMPyê + Serverên te + Adresa servera te + Servera te + Profîla te yî %1$s wê bê parvekirin. + Tercihên te + Serverên te yên ICEyê + Serverên te yên ICEyê + Koma te + te %1$s derxist + Te dewetiya komê red kir + Profîla te yî niha + Tu karî dûvre wê çêkî + Tu dikarî wê di Eyarên xuyakirinê de biguherî. + te %s blok kir + Tu hatiye dewetkirinî komê + Tu jixwe dikevî vê komê bi riya vê lînkê. + Tu dihêlî + te ev endam qebûl kir + tu: %1$s + TU + tu + Erê + erê + Serverên XFTPyê + Servera XFTPyê + Şîfra xelet! + Şîfra xelet ya databasê + Bi kêmtir xerckirina pîlê. + Bi kêmtir xerckirina pîlê. + Bê Tor yan VPNê, adresa te yî IPyê wê ji van relayên XFTPyê re xuya bike:\n%1$s, + Bê Tor yan jî VPNê, wê adresa te yî IPyê ji serverên dosyayen re xuya bike. + Etherneta bi qeblo + WiFi + Çi yî nû heye + Websîte + Serverên WebRTC ICEyê + Hişyarî: hinek dataya te kare winda bibe! + dixwaze bi te re bikeve danûstandinê! + Girtî + Bi xêra xwe aplîkasyonê ji nû ve veke. + Veşêre: + Navê profîlê: + Navê timam: + Qeyd bike û xeberê bide endamên komê + Şîfra nîşandanê + Şîfra profîla veşartî + Tu karî markdownê bişuxulînî ji bo formatkirina mesajan: + Bi şuxulandina SimpleX Chatê tu qebûl dikî ku tu:\n- di komên vekirî tenê muhtewaya qanûnî bişînî.\n- hurmeta karberên dî bigirî – spam çênabe. + Veke + bi riya relayê + Vidyo girtî + Vîdyo vekirî + Deng girtî + Deng vekirî + Ekrana aplîkasyonê biparêze + Ji ber xwe ve sûretan qebûl bike + Adresa IPyê biparêze + Girtî + Na + Bipirse + Ber lînka webê were vekirin? + Lînkê veke + Lînka timam veke + Lînka paqij veke + ARÎKARÎ + APLÎKASYON + DOSYA + Ji nû ve veke + PROKSIYA SOCKSÊ + Sûretên profîlan + Girêdana torê + Ji kompîterê bişuxulîne + Xeletiya databasê + Dosya: %s + Xeletî: %s + Xeletiya nenaskirî + dewetiya ji bo koma %1$s + Derkeve + Kom nehat dîtin! + Ev kom nema heye. + derket + tu derketî + %s û %s + %s, %s û %d endam + adresa ji bo te hat guhertin + te adresa ji bo %s guhert + te adres guhert + mielif + endam + moderator + xwedî + redkirî + derxistî + derketiye + nayê zanîn + Endam %1$s + Rola endamên nû + Rola pêşî + Ji komê derkeve + Lînka komê + Xeletî di şandina dewetiyê de + Halê dosyayê + Wextê standinê + Wextê nûkirina qeydiyê: %s + Halê dosyayê: %s + Wextê şandinê: %s + Wextê standinê: %s + nivîs nîne + Endam derxe? + Endaman derxe? + Endam derxe + Derxe + Endam derxe + Endam blok bike + Blok bike + Ji admîn blokkirî + blokkirî + ne aktîv + ENDAM + Rol + Kom + Te standin bi riya + Halê torê + Girêdanê biedilîne + Biedilîne + Navê timam î komê: + Profîla komê di cihazên endaman de qeydkirî ye, ne di serveran de. + Profîla komê qeyd bike + Serveran bişuxulîne + %s bişuxulîne + Li şertan meyzîne + Şertên nûkirî + %s bişuxulînî, şertên şuxulandinê qebûl bike.]]> + Ji bo dosyayan bişuxulîne + Serverên medya & dosyayê ên lêzedekirî + Şertan veke + Guhertinan veke + Protokola serverê hat guhertin. + Reqema PINGan + TCP keep-alive aktîv bike + Qeyd bike + Qeyd bike û dîse girê de + Eyarên torê nû bike? + Profîl lê zêde bike + Girêdanên profîl û serveran + Veşêre + Nîşan bide + Bêdeng bike + Bêdengkirinê betal bike + Profîlê bike şexsî! + Şîfra profîlê + Rehnik + Rehnik + Reş + Sernav + Cewaba standî + Sûret jê bibe + Mezinbûniya fontê + Şefafî + Êvara te bi xêr! + Sibeha te bi xêr! + Dagire + Endam karin dosya û medya bişînin. + Dosya û medya memnû in. + Endam karin lînkên SimpleXê bişînin. + %d san + %d heftî + UIa farisî + Eyarên nû yên medyayê + Aplîkasyonê bi yek destî bişuxulîne. + Mezinbûniya fontê zêde bike. + Sebeba qutbûna girêdanê: %s + Ev lînk bi telefoneke dî re hatiye şuxulandin, bi xêra xwe li kompîterê lînkeke nû çêke. + Girêdana bi kompîterê re qut bike? + Tenê yek cihaz kare di eynî wextî de bişuxule + Li hêviya girêdana telefonê: + Ji ber xwe ve girê de + %s ne aktîv e]]> + %s mijûl e]]> + %s re di halekî xirab de ye]]> + Siḧbetê veke + Xeletî di çêkirina lîsta siḧbetan de + Xeletî di vekirina siḧbetê de + Siḧbetê bisekinîne + Profîlên siḧbetê biguhere + Siḧbet + Ti siḧbetên te nînin + Ti siḧbet di lîsta %s de nînin. + Ti siḧbetên nexwendî nînin + Siḧbet nînin + Ti siḧbet nehatin dîtin + Siḧbeta hilbijartî nîne + Tiştekî hilbijartî nîne + %d hilbijartî + Favorît + Kom + %d siḧbetên bi endaman + 1 siḧbeta bi yek endamî + %d siḧbet + Robot + Kom + Navê siḧbetê deyne… + Siḧbeteke nû bide destpêkirin + Ji bo ko yek siḧbete nû bide destpêkirin + Siḧbet ber were valakirin? + Hemû mesaj wê bên jêbirin - ev nikare were betalkirin/vegerandin! Wê mesaj TENÊ ji bo te bên jêbirin. + Siḧbetê vala bike + Hemû siḧbet wê ji lîsta %s bên jêbirin, û wê lîste bê jêbirin + Siḧbeta nû + Profîla sihbetê hilbijêre + Profîlên te yên siḧbetê + Profîla siḧbetê çêke + Ber serverên SimpleX Chatê werin şuxulandin? + Profîla siḧbetê + Tu siḧbeta xwe qontrol dikî! + Siḧbetê bişuxulîne + SIḦBET + Rengên siḧbetê + Siḧbet sekinandî ye + DATABASA SIḦBETÊ + Ber siḧbet were sekinandin? + Xeletî di sekinandina siḧbetê de + Ber profîla siḧbetê were jêbirin? + ne ti carî + Şîfra databasê lazim e ji bo vekirina siḧbetê. + Şîfre qeyd bike û siḧbetê veke + Siḧbetê veke + Siḧbet sekinandî ye + Ber siḧbet were destpêkirin? + Tu dixwazî ji siḧbetê derkevî? + Siḧbetê jê bibe + Ber siḧbet were jêbirin? + Wê siḧbet ji bo te bê jêbirin - ev nikare were betalkirin/vegerandin! + Ji siḧbetê derkeve + Tenê xwediyên siḧbetê karin tercihan biguherin. + Bi admînan re siḧbetê bike + Bi endam re siḧbetê bike + Wê endêm ji siḧbetê bê derxistin - ev nikare were betalkirin/vegerandin! + Wê endam ji siḧbetê bên derxistin - ev nikare were betalkirin/vegerandin! + Siḧbet + Wê profîla te yî siḧbetê ji endamên komê re bê şandin + Wê profîla te yî siḧbetê ji endamên siḧbetê re bê şandin + Serverên ji bo dosyayên nû ên profîla te yî siḧbetê ya niha + Ber profîla siḧbetê were jêbirin? + Hemû mesaj wê bên jêbirin - ev nikare were betalkirin/vegerandin! + Profîla sihbetê jê bibe + Profîla siḧbetê hew veşêre + Vegerîne temaya aplîkasyonê + Vegerîne temaya karber + Temaya serî/pêşî diyar bike + Moda reḧnik + na + vekirî + girtî` + Tercihên siḧbetê + Mesajên ko winda dibin li vê siḧbetê nayên qebûlkirin. + Jêbirina ko nikare were betalkirin/vegerandin di vê siḧbetê de nayê qebûlkirin. + Siḧbetên bi endam + Ti siḧbetên bi endam nînin + Siḧbetê jê bibe + Bi admînan re siḧbetê bike + Siḧbetê ji nû ve veke + Siḧbet tê sekinandin + Siḧbetê bide destpêkirin + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 0b59cc1b06..2f26545913 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -25,12 +25,12 @@ zmoderowane przez %s wysyłanie plików nie jest jeszcze obsługiwane odbieranie plików nie jest jeszcze obsługiwane - Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu. + Próba połączenia z serwerem, który służył do odbierania wiadomości z tego połączenia. Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %1$s). nieznany format wiadomości SimpleX Ty - Jesteś połączony z serwerem używanym do odbierania wiadomości od tego kontaktu. + Jesteś połączony z serwerem, który służył do odbierania wiadomości z tego połączenia. Twój profil zostanie wysłany do kontaktu, od którego otrzymałeś ten link. udostępniłeś jednorazowy link incognito przez link grupowy @@ -71,10 +71,10 @@ Błąd usuwania kontaktu Błąd usuwania grupy Błąd usuwania oczekującego połączenia kontaku - Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy + Odcisk palca w adresie serwera nie pasuje do certyfikatu. Bezpieczna kolejka Nadawca mógł usunąć prośbę o połączenie. - Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło + Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło. Test nie powiódł się na etapie %s. Błąd usuwania profilu użytkownika Błąd aktualizacji prywatności użytkownika @@ -141,7 +141,7 @@ Usunąć wiadomość członka\? edytowana Dla wszystkich - dołącz jako %s + Dołącz jako %s wysyłanie nie powiodło się wyślij Udostępnij plik… @@ -154,7 +154,7 @@ oznacz jako nieprzeczytane Witaj! Witaj %1$s! - jesteś zaproszony do grupy + Jesteś zaproszony do grupy Nie masz czatów Czaty Poproszony o odbiór obrazu @@ -179,7 +179,7 @@ Oczekiwanie na film Oczekiwanie na film jesteś obserwatorem - Nie możesz wysyłać wiadomości! + Jesteś obserwatorem Połączony Obecnie maksymalny obsługiwany rozmiar pliku to %1$s. Usuń kontakt @@ -428,9 +428,9 @@ Jak to działa Jak SimpleX działa Natychmiastowy - Można to później zmienić w ustawieniach. + Jak wpływa na baterię Nawiąż prywatne połączenie - dwuwarstwowego szyfrowania end-to-end.]]> + Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości. Okresowo Prywatne powiadomienia repozytorium GitHub.]]> @@ -814,10 +814,10 @@ %d mies %ds %d sek - Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny) - Członkowie grupy mogą wysyłać bezpośrednie wiadomości. + Członkowie mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny) + Członkowie mogą wysyłać bezpośrednie wiadomości. Nieodwracalne usuwanie wiadomości jest na tym czacie zabronione. - Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione. + Usuwanie wiadomości nieodwracalnych jest zabronione. Tylko Ty możesz nieodwracalnie usunąć wiadomości (Twój kontakt może oznaczyć je do usunięcia). (24 godziny) Tylko Ty możesz wysyłać znikające wiadomości. Tylko Ty możesz wysyłać wiadomości głosowe. @@ -827,7 +827,7 @@ Zabroń wysyłania bezpośrednich wiadomości do członków. Zabroń wysyłania znikających wiadomości. Wiadomości głosowe są zabronione na tym czacie. - Wiadomości głosowe są zabronione w tej grupie. + Wiadomości głosowe są zabronione. Administratorzy mogą tworzyć linki do dołączania do grup. Automatyczne akceptowanie próśb o kontakt anulowano %s @@ -921,7 +921,7 @@ %d dni Usuń Usuń wiadomości po - Znikające wiadomości są zabronione w tej grupie. + Znikające wiadomości są zabronione. Błąd usuwania prośby o kontakt Nie znaleziono pliku Błąd zapisu serwerów SMP @@ -939,9 +939,9 @@ zaproponował %s: %2s Tylko właściciele grup mogą włączyć wiadomości głosowe. Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia). (24 godziny) - Hasło nie zostało znalezione w Keystore, wprowadź je ręcznie. Może się tak zdarzyć, gdy przywrócisz dane aplikacji za pomocą narzędzia do kopii zapasowych. Jeśli tak nie jest, skontaktuj się z programistami. - Członkowie grupy mogą wysyłać znikające wiadomości. - Członkowie grupy mogą wysyłać wiadomości głosowe. + Hasło nie znalezione w Keystore, proszę wpisać je ręcznie. Mogło się to zdarzyć, jeśli przywróciłeś dane aplikacji za pomocą narzędzia do tworzenia kopii zapasowej. Jeśli tak nie jest, skontaktuj się z deweloperami. + Członkowie mogą wysyłać znikające wiadomości. + Członkowie mogą wysyłać wiadomości głosowe. Grupa zostanie usunięta dla wszystkich członków - nie można tego cofnąć! Jak korzystać z Twoich serwerów zeskanować kod QR w rozmowie wideo, lub Twój rozmówca może udostępnić link z zaproszeniem.]]> @@ -951,7 +951,7 @@ Zaimportować bazę danych czatu\? Tryb incognito chroni Twoją prywatność używając nowego losowego profilu dla każdego kontaktu. pośrednie (%1$s) - pozwolić SimpleX na działanie tle w następnym oknie dialogowym. W przeciwnym razie powiadomienia zostaną wyłączone.]]> + Pozwól w następnym oknie dialogowym natychmiast otrzymywać powiadomienia.]]> Zainstaluj SimpleX Chat na terminal Nieprawidłowe potwierdzenie migracji zaproszenie do grupy %1$s @@ -985,8 +985,8 @@ Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną. Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online. Ten link nie jest prawidłowym linkiem połączenia! - SimpleX - zużywa ona kilka procent baterii dziennie.]]> - Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów. + SimpleX działa w tle zamiast korzystać z powiadomień push.]]> + Aby chronić Twoją prywatność, SimpleX używa oddzielnych identyfikatorów dla każdego z Twoich kontaktów. Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach. Użyj dla nowych połączeń O ile Twój kontakt nie usunął połączenia lub ten link był już użyty, może to być błąd - zgłoś go. @@ -1142,7 +1142,7 @@ Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. Utwórz adres, aby ludzie mogli się z Tobą połączyć. Utwórz adres SimpleX - Zapisz ustawienia automatycznej akceptacji + Zapisz ustawienia adresów SimpleX Udostępnij kontaktom Możesz go utworzyć później Adres @@ -1173,10 +1173,10 @@ Wyślij znikającą wiadomość Zabroń reakcje wiadomości. Reakcje wiadomości - Członkowie grupy mogą dodawać reakcje wiadomości. + Członkowie mogą dodawać reakcje na wiadomości. godziny Reakcje wiadomości są zabronione na tym czacie. - Reakcje wiadomości są zabronione w tej grupie. + Reakcje na wiadomości są zabronione. minuty miesiące Tylko Ty możesz dodawać reakcje wiadomości. @@ -1245,9 +1245,9 @@ Pozwól na wysyłanie plików i mediów. Brak filtrowanych czatów Nieulubione - Członkowie grupy mogą wysyłać pliki i media. + Członkowie mogą wysyłać pliki i media. Zakaz wysyłania plików i mediów. - Pliki i media są zabronione w tej grupie. + Pliki i media są zabronione. Tylko właściciele grup mogą włączać pliki i media. Szukaj Wyłączono @@ -1378,7 +1378,7 @@ Błąd tworzenia kontaktu członka Wyślij wiadomość bezpośrednią aby połączyć wyślij wiadomość bezpośrednią - połącz bezpośrednio + Prośba o połączenie usunięto kontakt Utwórz grupę Utwórz profil @@ -1556,7 +1556,7 @@ członek %1$s zmienił na %2$s ustaw nowy adres kontaktu zaktualizowano profil - Były członek %1$s + Członek %1$s Zablokować członka dla wszystkich? Utworzony o Zachowano wiadomość @@ -1712,7 +1712,7 @@ Wiadomości głosowe są niedozwolone Włączony dla właściciele - Linki SimpleX są zablokowane na tej grupie. + Linki SimpleX są zablokowane. Inne WiFi Połączenie ethernet (po kablu) @@ -1720,7 +1720,7 @@ wszyscy członkowie Zezwól na wysyłanie linków SimpleX. Sieć komórkowa - Członkowie grupy mogą wysyłać linki SimpleX. + Członkowie mogą wysyłać linki SimpleX. Brak połączenia z siecią Zabroń wysyłania linków SimpleX Linki SimpleX @@ -1929,7 +1929,7 @@ Wyświetlanie informacji dla Statystyki Sesje transportowe - Zaczynanie od %s. \nWszystkie dane są prywatne na Twoim urządzeniu. + Zaczynanie od %s.\nWszystkie dane są prywatne na Twoim urządzeniu. Połącz ponownie wszystkie serwery Połączyć ponownie serwer? Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości. @@ -2040,7 +2040,7 @@ Zaznacz Wiadomości zostaną usunięte dla wszystkich członków. Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków. - Osiągalny pasek narzędzi czatu + Osiągalny pasek narzędzi Wyeksportowano bazę danych czatu Kontynuuj Serwery mediów i plików @@ -2094,7 +2094,7 @@ Dźwięk wyciszony Wybierz profil czatu Udostępnij profil - Twoje połączenie zostało przeniesione do %s, ale podczas przekierowania do profilu wystąpił nieoczekiwany błąd. + Twoje połączenie zostało przeniesione na %s, ale pojawił się błąd podczas zmiany profilu. Tryb systemu Przesłane archiwum bazy danych zostanie trwale usunięte z serwerów. Serwer @@ -2189,4 +2189,369 @@ Nowy członek chce dołączyć do grupy. 1 rok Akceptuj + rozmowa z członkiem grupy + Akceptuj + Akceptuj jako członek grupy + Akceptuj jako obserwator grupy + Akceptuj dodanie kontaktu + Akceptuj dodanie kontaktu + Akceptuj użytkownika grupy + Dodaj wiadomość + Wszystko + Wszystkie nowe wiadomości od tego użytkownika będą ukryte + Zezwól na wszystkie pliki i media tylko jeśli twój kontakt na to pozwala. + Zezwól na zgłaszanie raportów do moderatorów. + Zezwól twoim kontaktom na wysyłanie plików i mediów. + Zezwól na archiwizowanie raportów dla ciebie. + Wszystkie serwery + Zarchiwizować wszystkie raporty? + Zarchiwizować %d raportów? + Archiwizuj raporty + Lepsze działanie grupy + Lepsza prywatność i bezpieczeństwo + Opis: + Opis zbyt duży + Bot + Ty i twój kontakt możecie wysyłać pliki i media. + Kontakt służbowy + Uzywając SimpleX Chat zgadzasz się na:\n- wysyłanie tylko prawnie dopuszczonych treści na publicznych grupach.\n- szanowanie innych użytkowników - nie wysyłanie SPAM-u + Nie mogę zmienić profilu + nie mogę wysłać wiadomości + 4 nowe języki interfejsu + Wszystkie wiadomości + Blokowanie członków dla wszystkich? + Kataloński, indonezyjski, rumuński i wietnamski - dzięki naszym użytkownikom! + szyfrowanie end-to-end.]]> + tylko po zaakceptowaniu twojego żądania.]]> + Zmienić automatyczne usuwania wiadomości? + Czaty z członkami + Czat z administratorami + Czar z administratorami + Czat z administratorami + Czat z członkiem + Czatuj z członkami, zanim dołączą. + Konfigurowanie operatorów serwerów + Połącz + Połącz się szybciej! 🚀 + kontakt usunięty + kontakt zablokowany + kontakt nie gotowy + PROŚBY O KONTAKT OD GRUP + kontakt powinien zaakceptować… + Stwórz swój adres + %d czat(y) + %d czaty z członkami + domyślny (%s) + Usuń czat + Usuń wiadomości czatu z urządzenia. + Skasować czat z tym członkiem? + Skasuj wiadomości od tego członka + Skasować wiadomości od tego członka? + Skasuj wiadomości + Opcje wycofane + Opis jest zbyt duży + Bezpośrednie wiadomości między członkami są zabronione. + Wiadomości bezpośrednie między członkami są zabronione na tym czacie. + Wyłączyć automatyczne usuwanie wiadomości? + Zablokuj skasowane wiadomości + %d wiadomości + Nie przegap ważnych wiadomości. + %d raporty + Edytuj + Włącz domyślne znikanie wiadomości. + Włącz Flux w ustawieniach sieci i serwerów, aby uzyskać lepszą prywatność metadanych. + Włącz logi + Renegocjacja szyfrowania jest w toku. + Błąd podczas akceptacji warunków + Błąd podczas akceptacji członka + Błąd podczas dodawania serwera + Błąd podczas zmiany profilu + Błąd podczas tworzenia listy czatu + Błąd podczas tworzenia raportu + Błąd usuwania czatu + Błąd ładowania list czatu + Błąd oznaczania odczytu + Błąd otwierania czatu + Błąd otwierania grupy + Błąd odczytu bazy danych hasła + Błąd odrzucenia prośby o kontakt + Błąd zapisywania bazy danych + Błąd zapisywania serwerów + Błąd zapisywania ustawień + Błąd w konfiguracji serwerów. + Błąd aktualizowania listy czatu + Błąd aktualizacji serwera + Szybsze usuwania grup. + Szybsze wysyłanie wiadomości. + Ulubione + Plik jest zablokowany przez operatora serwera:\n%1$s. + Pliki + Pliki i media są zabronione na tym czacie. + Filtr + Odcisk palca w docelowym serwerze nie pasuje do certyfikatu: %1$s. + Odcisk palca w adresie serwera nie pasuje do certyfikatu: %1$s. + Odcisk palca w adresie serwera nie pasuje do certyfikatu: %1$s. + Napraw + Naprawić połączenie? + Dla wszystkich moderatorów + Lepsza prywatność metadanych. + Dla profilu czatu %s: + Na przykład, jeśli kontakt otrzyma wiadomości za pośrednictwem serwera czatu SimpleX, aplikacja dostarczy je za pośrednictwem serwera Flux. + Dla mnie + Dla prywatnego routingu + Dla mediów społecznościowych + Pełny link + Otrzymaj powiadomienie jeśli ktoś wspomni. + Grupa + grupa została usunięta + Grupy + Pomóż administratorom moderować ich grupy. + Jak to pomaga prywatności + Zdjęcia + Poprawiona nawigacja czatu + Niewłaściwa zawartość + Niewłaściwy profil + Zaproszenie do czatu + Dołącz do grupy + Zachowaj swoje czaty czyste + Opuść czat + Opuścić czat? + Mniejszy ruch w sieciach mobilnych. + Linki + Lista + Lista imion... + Nazwa i emoji powinny być inne dla wszystkich list. + Wczytywanie profilu… + Przyjęcie członkostwa + członek posiada starą wersję + Członek został usunięty - nie można przyjąć żądania + Wiadomości członkowskie zostaną usunięte - nie można tego cofnąć! + Raporty członkowskie + Członkowie mogą zgłaszać wiadomości moderatorom. + Członkowie zostaną usunięci z czatu - tego nie da się cofnąć! + Członkowie zostaną usunięci z grupy - nie można tego cofnąć! + Członek zostanie usunięty z czatu - nie można tego cofnąć! + Członek dołączy do grupy, czy zaakceptować tego członka? + Wspomnij członka 👋 + Wyślij wiadomość natychmiast po dotknięciu Połącz. + Wiadomość jest za duża! + Wiadomości od tych członków zostaną pokazane! + Wiadomości na tym czacie nigdy nie zostaną usunięte. + moderator + moderatorzy + Wycisz wszystko + Decentralizacja sieci + Operator sieci + Operatorzy sieci + Nowa rola w grupie: Moderator + Nowy serwer + Nie + Brak usług w tle + Żadnych czatów + Nie znaleziono żadnych czatów + Nie ma czatów na liście %s. + Żadnych rozmów z członkami + Brak mediów i serwerów plików multimedialnych. + Brak wiadomości + Brak serwerów wiadomości. + Brak prywatnej sesji routingu + Brak serwerów prywatnej sesji routingu + Brak serwerów do otrzymania plików. + Brak serwerów aby otrzymać wiadomości. + Brak serwerów do wysyłania plików. + brak subskrypcji + Notatki + Powiadomienia i bateria + nie zsynchronizowano + Brak nieprzeczytanych czatów + wyłączony + Wyłącz + Tylko właściciele czatu mogą zmieniać preferencje. + Widzą to tylko nadawca i moderatorzy + Widzisz to tylko Ty i moderatorzy + Tylko Ty możesz wysyłać pliki i multimedia. + Tylko Twój kontakt może wysyłać pliki i multimedia. + Otwórz zmiany + Otwórz czat + - Otwórz czat w pierwszej nieprzeczytanej wiadomości.\n- Przejdź do cytowanych wiadomości. + Otwórz czysty link + Otwórz warunki + Otwórz pełen link + Otwórz link + Otwórz linki z listy czatów + Otwórz nowy czat + Otwórz nową grupę + Otwórz by zaakceptować + Otwórz aby się połączyć + Otwórz aby dołączyć + Otwórz aby skorzystać z bota + Otworzyć link sieci web? + Otwórz z %s + Operator + Serwer Operatora + Organizuj czaty jako listy + Lub zaimportuj plik archiwalny + Lub udostępnij prywatnie + Nie można odczytać hasła w magazynie kluczy. Wprowadź je ręcznie. Mogło się to zdarzyć po aktualizacji systemu niezgodnej z aplikacją. Jeśli tak nie jest, skontaktuj się z programistami. + Nie można odczytać hasła w magazynie kluczy. Mogło się to zdarzyć po aktualizacji systemu niezgodnej z aplikacją. Jeśli tak nie jest, skontaktuj się z programistami. + oczekuje + oczekiwanie zaakceptowane + oczekująca recenzja + Zmniejsz rozmiar wiadomości i wyślij ją ponownie. + Zmniejsz rozmiar wiadomości lub usuń multimedia i wyślij ponownie. + Poczekaj, aż moderatorzy grupy rozpatrzą Twoją prośbę o dołączenie do grupy. + Domyślne serwery + Domyślne serwery + Prywatność dla Twoich klientów. + Polityka prywatności i warunki korzystania. + Prywatne czaty, grupy i Twoje kontakty nie są dostępne dla operatorów serwerów. + Nazwy prywatnych plików multimedialnych. + Limit czasu routingu prywatnego + Zabroń raportowania wiadomości moderatorom. + Zabroń wysyłania plików i multimediów. + Limit czasu protokołu w tle + Dostępny pasek narzędzi czatu + Odrzuć + Odrzuć prośbę o kontakt + odrzucono + odrzucono + Odrzucić członka? + Zdalne telefony komórkowe + Usuń i skasuj wiadomości + przeniesiono z grupy + Usuń śledzenie linków + Usunąć członka? + Usuwa wiadomości i blokuje członków. + Zgłoś + Zgłoś treść: zobaczą ją tylko moderatorzy grupy. + Na tej grupie zabronione jest zgłaszanie wiadomości. + Zgłoś profil członka: będą go widzieć tylko moderatorzy grupy. + Zgłoś inne: zobaczą to tylko moderatorzy grupy. + Jaki jest powód zgłoszenia? + Zgłoś: %s + Zgłoszenia + Zgłoszenia wysłane do moderatorów + Zgłoś spam: tylko moderatorzy grupy będą to widzieć. + Zgłoś naruszenie: zobaczą je tylko moderatorzy grupy. + Prośba o połączenie od grupy %1$s + poproszono o połączenie + prośba została wysłana + prośba o dołączenie została odrzucona + przejrzyj + Przejrzyj warunki + sprawdzone przez administratorów + Przejrzyj członków grupy + Przejrzyj później + Przejrzyj członków + Przejrzyj członków przed przyjęciem (pukanie). + Zapisać ustawienia wstępu? + Zachowaj listę + Poszukaj plików + Poszukaj obrazów + Poszukaj linków + Poszukaj wideo + Poszukaj wiadomości głosowych + Wybierz operatora sieci + Wysłać prośbę o kontakt? + Wyślij prywatne zgłoszenia + Wyślij prośbę + Wyślij prośbę bez wiadomości + Wyślij swoją prywatną opinię do grup. + Wysłano do Twojego kontaktu po połączeniu. + Serwer dodany do operatora %s. + Operator serwera zmieniony. + Operatorzy serwera + Protokół serwera zmieniony. + Ustaw nazwę czatu… + Ustaw przyjęcie członka + Ustaw datę wygaśnięcia wiadomości na czatach. + Ustaw biografię profilu i wiadomość powitalną. + Udostępnij adres publicznie + Udostępnij stary adres + Udostępnij stary link + Udostępnij adres SimpleX w mediach społecznościowych. + Udostępnij swój adres + Krótki opis: + Krótki link + Krótki adres SimpleX + Link do kanału na SimpleX + SimpleX Chat i Flux zawarły umowę na włączenie do aplikacji serwerów obsługiwanych przez Flux. + łącze przekaźnikowe SimpleX + Spam + Spam + %s serwery + Dotknij Połącz aby rozpocząć czat + Dotknij Połącz, aby wysłać prośbę + Dotknij Połącz aby użyć bota + Dotknij Stwórz adres SimpleX w menu aby utworzyć go później. + Dotknij Dołącz do grupy + Przekroczono limit czasu połączenia TCP + Port TCP dla wiadomości + Adres będzie krótki, a Twój profil zostanie udostępniony za pośrednictwem adresu. + Aplikacja chroni Twoją prywatność, korzystając z różnych operatorów w każdej rozmowie. + Połączenie osiągnęło limit niedostarczonych wiadomości, Twój kontakt może być offline. + Link będzie krótki, a profil grupowy zostanie udostępniony poprzez link. + Raport zostanie dla Ciebie zarchiwizowany. + Rola zostanie zmieniona na %s. Wszyscy uczestnicy czatu zostaną powiadomieni. + Drugi predefiniowany operator w aplikacji! + Nadawca NIE zostanie poinformowany. + Serwery dla nowych plików Twojego bieżącego profilu czatu + Tej akcji nie można cofnąć - wiadomości wysłane i otrzymane na tym czacie wcześniej niż wybrane zostaną usunięte. + Ten link wymaga nowszej wersji aplikacji. Zaktualizuj aplikację lub poproś osobę kontaktową o przesłanie kompatybilnego łącza. + Ta wiadomość została usunięta lub jeszcze nie otrzymana. + To ustawienie jest dla Twojego obecnego profilu. + Czas zniknięcia jest ustawiony tylko dla nowych kontaktów. + Aby zabezpieczyć się przed wymianą łącza, możesz porównać kody bezpieczeństwa kontaktu. + Żeby odebrać + Żeby wysłać + Aby wysyłać polecenia, musisz być podłączony. + Aby po próbie połączenia skorzystać z innego profilu, usuń czat i użyj linku ponownie. + Przeźroczystość + Odblokować członków dla wszystkich? + Niedostarczone wiadomości + Nieprzeczytane wzmianki + Nieobsługiwane łącze połączenia + Aktualizacja + Warunki aktualizacji + Aktualizuj swój adres + Upgrade + Uaktualnij adres + Uaktualnić adres? + Uaktualnij link do grupy + Uaktualnić link do grupy? + Użyj dla plików + Użyj dla wiadomości + Użyj profilu incognito + Użyj %s + Użyj serwerów + Użyj portu TCP %1$s, jeśli nie określono żadnego portu. + Używaj portu TCP 443 tylko dla wstępnie ustawionych serwerów. + Użyj portu internetowego + Wideo + Zobacz warunki + Zobacz zaktualizowane warunki + Wiadomości głosowe + Strona Internetowa + Wiadomość powitalna + Powitaj swoje kontakty + Gdy włączony jest więcej niż jeden operator, żaden z nich nie ma metadanych pozwalających dowiedzieć się, kto się z kim komunikuje. + Tak + zaakceptowałeś tego członka + Nie masz połączenia z serwerem używanym do odbierania wiadomości z tego połączenia (brak subskrypcji). + Możesz skonfigurować operatorów w ustawieniach sieci i serwerów. + Serwery można skonfigurować w ustawieniach. + Możesz skopiować i zmniejszyć rozmiar wiadomości, aby ją wysłać. + Możesz wzmiankować do %1$s członków na wiadomość! + Możesz ustawić nazwę połączenia, aby zapamiętać, z kim link został udostępniony. + Nie możesz wysyłać wiadomości! + Możesz przeglądać swoje raporty na czacie z administratorami. + odszedłeś + Twój opis: + Twój kontakt biznesowy + Twój profil na czacie zostanie wysłany do członków czatu + Twój kontakt + Twoja grupa + Twój profil + Twoje serwery + Przestaniesz otrzymywać wiadomości z tego czatu. Historia czatu zostanie zachowana. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 95e5ed9409..5445c57055 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -340,7 +340,7 @@ Одноразовая ссылка Настройки - Ваш SimpleX адрес + Ваш адрес SimpleX База данных Подробнее о SimpleX Chat Как использовать @@ -428,7 +428,7 @@ Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве. Профиль отправляется только Вашим контактам. Имя профиля не может содержать пробелы. - Введите ваше имя: + Имя: Создать О SimpleX @@ -496,7 +496,7 @@ видеозвонок аудиозвонок - Аудио- и видеозвонки + Аудио и видеозвонки Ваши звонки Всегда соединяться через relay Звонки на экране блокировки: @@ -627,7 +627,7 @@ Текущий пароль… Новый пароль… Подтвердите новый пароль… - Поменять пароль + Сменить пароль Пожалуйста, введите правильный пароль. База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные. Android Keystore используется для безопасного хранения пароля - это позволяет стабильно получать уведомления в фоновом режиме. @@ -636,7 +636,7 @@ Пароль базы данных будет безопасно сохранён в Android Keystore после запуска чата или изменения пароля - это позволит стабильно получать уведомления. Пароль не сохранён на устройстве — Вы будете должны ввести его при каждом запуске чата. Зашифровать базу данных? - Поменять пароль базы данных? + Сменить пароль базы данных? База данных будет зашифрована. База данных будет зашифрована и пароль сохранён в Keystore. Пароль базы данных будет изменён и сохранён в Keystore. @@ -660,7 +660,7 @@ Введите пароль… Сохранить пароль и открыть чат Открыть чат - Попытка поменять пароль базы данных не была завершена. + Попытка изменить пароль базы данных не была завершена. Восстановить резервную копию Восстановить резервную копию? Введите предыдущий пароль после восстановления резервной копии. Это действие нельзя отменить. @@ -713,7 +713,7 @@ Вы поменяли роль себе на: %s Вы удалили %1$s Вы покинули группу - профиль группы обновлен + профиль группы обновлён поменял(а) адрес для Вас смена адреса… @@ -818,7 +818,7 @@ Включить TCP keep-alive Сохранить Обновить настройки сети? - Обновление настроек приведет к переподключению клиента ко всем серверам. + Обновление настроек приведёт к переподключению клиента ко всем серверам. Обновить Инкогнито @@ -880,7 +880,7 @@ Прямые сообщения между членами группы запрещены. Члены могут необратимо удалять отправленные сообщения. (24 часа) Необратимое удаление сообщений запрещено. - Члены могут отправлять голосовые сообщения. + Участники могут отправлять голосовые сообщения. Голосовые сообщения запрещены. Минимальный расход батареи. Вы получите уведомления только когда приложение запущено, без фонового сервиса.]]> Уведомления @@ -981,7 +981,7 @@ Только локальные данные профиля Сообщения Серверы для новых соединений Вашего текущего профиля чата - Ваши профили + Профили Все чаты и сообщения будут удалены - это нельзя отменить! Сборка приложения: %s Версия приложения: v%s @@ -1059,7 +1059,7 @@ Установите приветственное сообщение для новых членов группы. Нажмите на профиль, чтобы переключиться на него. Благодаря пользователям - добавьте переводы через Weblate! - Вы все равно получите звонки и уведомления в профилях без звука, когда они активные. + Вы всё равно получите звонки и уведомления в профилях без звука, когда они активные. Вы можете скрыть или отключить уведомления профиля - нажмите и удерживайте профиль, чтобы открыть меню. Изображение будет принято когда Ваш контакт его загрузит. Файл будет принят когда Ваш контакт загрузит его. @@ -1074,7 +1074,7 @@ версия базы данных новее чем приложения, но нет миграции для отката: %s разная миграция в приложении/базе данных: %s / %s Откатить версию и открыть чат - Предупреждение: Вы можете потерять какие то данные! + Предупреждение: Вы можете потерять некоторые данные! ID базы данных и опция Отдельные транспортные сессии. Показать опции для разработчиков Удалить профиль чата @@ -1203,7 +1203,7 @@ Изменить код самоуничтожения Самоуничтожение Код самоуничтожения включен! - Код доступа в приложение будет заменен кодом самоуничтожения. + Код доступа в приложение будет заменён кодом самоуничтожения. Включить код самоуничтожения Код самоуничтожения Код самоуничтожения изменен! @@ -1243,13 +1243,13 @@ Ваши контакты сохранятся. Настроить тему Создайте адрес, чтобы можно было соединиться с Вами. - Все Ваши контакты сохранятся. Обновленный профиль будет отправлен Вашим контактам. + Все Ваши контакты сохранятся. Обновлённый профиль будет отправлен Вашим контактам. Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам. Создать адрес SimpleX Поделиться с контактами Прекратить делиться адресом\? Автоприём - Введите приветственное сообщение... (опционально) + Введите приветственное сообщение... (по желанию) Сохранить настройки\? Прекратить делиться Продолжить @@ -1373,7 +1373,7 @@ Пересогласовать шифрование Быстрый поиск чатов Отчёты о доставке сообщений! - Еще несколько изменений + Ещё несколько изменений Отчёты о доставке! Включить Даже когда они выключены в разговоре. @@ -1622,7 +1622,7 @@ Ошибка создания сообщения Ошибка удаления заметки Венгерский и Турецкий интерфейс - Искать или вставьте ссылку SimpleX + Поиск или вставить ссылку SimpleX Этот QR-код не является SimpleX-ccылкой. С зашифрованными файлами и медиа. С уменьшенным потреблением батареи. @@ -1682,15 +1682,15 @@ Сохранённое сообщение неизвестно неизвестный статус - %d сообщений заблокировано администратором + %d сообщений заблокировано админом %s заблокирован %s разблокирован Вы разблокировали %s Разблокировать для всех Заблокировать члена группы для всех? заблокирован - заблокировано администратором - Заблокирован администратором + заблокировано админом + Заблокирован админом Заблокировать для всех Ошибка при блокировании члена группы для всех Разблокировать члена группы для всех? @@ -1750,7 +1750,7 @@ Завершить миграцию Или передайте эту ссылку Миграция завершена - Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений. + Внимание: запуск чата на нескольких устройствах не поддерживается и приведёт к сбоям доставки сообщений. не должны использовать одну и ту же базу данных на двух устройствах.]]> Проверьте подключение к Интернету и повторите попытку Подтвердите, что Вы помните пароль базы данных для её миграции. @@ -1815,7 +1815,7 @@ Ссылки SimpleX Разрешить отправлять ссылки SimpleX. Запретить отправку ссылок SimpleX - Члены могут отправлять SimpleX ссылки. + Участники могут отправлять ссылки SimpleX. админы все члены владельцы @@ -1836,9 +1836,9 @@ ФАЙЛЫ Новые темы чатов нет - Светлая - Системная - Цвета тёмного режима + Светлый + Системный + Цвета темного режима Получайте файлы безопасно Конфиденциальная доставка 🚀 Улучшенная доставка сообщений @@ -1871,7 +1871,7 @@ Всегда Подтверждать файлы с неизвестных серверов. Всегда использовать конфиденциальную доставку. - Тёмная + Тёмный Отладка доставки Ошибка инициализации WebView. Обновите Вашу систему до новой версии. Свяжитесь с разработчиками. \nОшибка: %s @@ -1948,7 +1948,7 @@ Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален. Выбранные настройки чата запрещают это сообщение. Ошибка файла - Сканировать / Вставить ссылку + Сканировать QR-код/ Вставить ссылку Другие XFTP-серверы Настроенные XFTP-серверы Загрузка %s (%s) @@ -2016,7 +2016,7 @@ Слабое Среднее Выключено - Доступная панель приложения + Панель приложения внизу Текущий профиль Нет информации, попробуйте перезагрузить Информация о серверах @@ -2217,7 +2217,7 @@ %s.]]> %s.]]> %s, примите условия использования.]]> - Для оправки + Для отправки Дополнительные серверы сообщений Использовать для файлов Открыть условия @@ -2341,11 +2341,11 @@ Ошибка обновления списка чата Ошибка создания списка чатов Список - Никаких чатов в списке %s. + Нет чатов в списке %s. Без непрочитанных чатов Никаких чатов Чаты не найдены - Все чаты будут удалены из списка %s, а сам список удален + Все чаты будут удалены из списка %s, а сам список удалён Добавить список Примечания Открыть в %s @@ -2380,7 +2380,7 @@ Улучшенная приватность и безопасность Ускорено удаление групп. Ускорена отправка сообщений. - Помогайте администраторам модерировать их группы. + Помогайте админам модерировать их группы. Организуйте чаты в списки Вы можете сообщить о нарушениях Установите время исчезания сообщений в чатах. @@ -2426,7 +2426,7 @@ модератор ожидает утверждения ожидает - Обновленные условия + Обновлённые условия Запретить жаловаться модераторам группы. Члены группы могут пожаловаться модераторам. Сообщения в этом чате никогда не будут удалены. @@ -2566,7 +2566,7 @@ Член группы удалён - невозможно принять запрос Чтобы использовать другой профиль после попытки соединения, удалите чат и используйте ссылку снова. Приветственное сообщение - О Вас: + О себе: Ваш профиль Описание слишком длинное Использовать профиль инкогнито @@ -2615,4 +2615,23 @@ Хэш в адресе сервера не соответствует сертификату: %1$s. Ссылка SimpleX relay Хэш в адресе сервера назначения не соответствует сертификату: %1$s. + Удалить сообщения участника + Удалить сообщения участника? + Удалить сообщения + Сообщения участника будут удалены - это действие не обратимо! + нет подписки + Вы не подключенны к серверу через который Вы получали сообщения от этого контакта (без подписки). + Удалить члена группы и удалить сообщения + Все сообщения + Файлы + Фильтр + Изображения + Ссылки + Поиск файлов + Поиск изображений + Поиск ссылок + Поиск видео + Поиск голосовых сообщений + Видео + Голосовые сообщения diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/sv/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/sv/strings.xml new file mode 100644 index 0000000000..55344e5192 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/sv/strings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 501f07ea50..16d821637b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -836,7 +836,7 @@ Mesaj tepkileri Tercihleriniz Mesaj tepkileri yasaklıdır. - Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız. + Bu bağlantıdan gelen mesajları almak için kullanılan sunucuya bağlısınız. Zaten %1$s e bağlısınız Doğrulanamadınız; lütfen tekrar deneyin. SimpleX Kilidini Ayarlar üzerinden açabilirsiniz. @@ -938,7 +938,7 @@ kapalı açık Kilit modunu değiştir - Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor. + Bu bağlantıdan gelen mesajları almak için kullanılan sunucuya bağlanmayı dene. Lütfen doğru bağlantıyı kullandığınızı kontrol edin veya irtibat kişinizden size başka bir bağlantı göndermesini isteyin. SimpleX arka planda çalışır.]]> Periyodik bildirimler @@ -2509,4 +2509,7 @@ Komutlar gönderebilmek için bağlanmanış olmanız gereklidir. Üye silinmiş - isteği kabul edemeyecek Grup linkini güncelle + Abonelik yok + Bu bağlantıdan mesaj almak için kullanılan sunucuya bağlı değilsiniz (abonelik yok). + SimpleX Relay Linki diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 5f729d4ea3..3e5e09c039 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -2525,4 +2525,21 @@ 服务器地址证书和证书不匹配:%1$s。 无订阅 未连接到用于从该连接接收消息的服务器(无订阅)。 + 删除成员消息 + 删除成员消息吗? + 删除消息 + 成员消息将被删除 - 这无法撤销! + 移除并删除消息 + 所有消息 + 文件 + 筛选器 + 图片 + 链接 + 搜索文件 + 搜索图片 + 搜索链接 + 搜索视频 + 搜索语音消息 + 视频 + 语音消息 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index d0ba082adf..3a93df406d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -21,13 +21,20 @@ import kotlin.math.sqrt private fun errorBitmap(): ImageBitmap = ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="))).toComposeImageBitmap() +private val base64BitmapCache = Collections.synchronizedMap(object : LinkedHashMap(200, 0.75f, true) { + override fun removeEldestEntry(eldest: Map.Entry): Boolean = size > 200 +}) + actual fun base64ToBitmap(base64ImageString: String): ImageBitmap { + base64BitmapCache[base64ImageString]?.let { return it } val imageString = base64ImageString .removePrefix("data:image/png;base64,") .removePrefix("data:image/jpg;base64,") return try { - ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap() - } catch (e: IOException) { + ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap().also { + base64BitmapCache[base64ImageString] = it + } + } catch (e: Throwable) { Log.e(TAG, "base64ToBitmap error: $e") errorBitmap() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index 9be10a584b..ed2f6e7859 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -23,6 +23,7 @@ private const val SERVER_HOST = "localhost" private const val SERVER_PORT = 50395 val connections = ArrayList() +// Spec: spec/services/calls.md#ActiveCallView @Composable actual fun ActiveCallView() { val scope = rememberCoroutineScope() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt index 38054cb873..b4a24e3572 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.chat.item import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import chat.simplex.common.model.CIFile import chat.simplex.common.platform.* @@ -17,7 +18,7 @@ actual fun SimpleAndAnimatedImageView( ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) { // LALAL make it animated too - ImageView(imageBitmap.toAwtImage().toPainter()) { + ImageView(BitmapPainter(imageBitmap)) { if (getLoadedFilePath(file) != null) { ModalManager.fullscreen.showCustomModal(animated = false) { close -> ImageFullScreenView(imageProvider, close) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index d541a5780e..8d69607c62 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.delay import java.io.ByteArrayInputStream import java.io.File import java.net.URI +import java.util.* import javax.imageio.ImageIO import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi @@ -128,6 +129,14 @@ actual fun getAppFileUri(fileName: String): URI { } } +private val loadedImageCache = Collections.synchronizedMap(object : LinkedHashMap>(30, 0.75f, true) { + override fun removeEldestEntry(eldest: Map.Entry>): Boolean = size > 30 +}) + +actual fun clearImageCaches() { + loadedImageCache.clear() +} + actual suspend fun getLoadedImage(file: CIFile?): Pair? { var filePath = getLoadedFilePath(file) if (chatModel.connectedToRemote() && filePath == null) { @@ -135,10 +144,10 @@ actual suspend fun getLoadedImage(file: CIFile?): Pair? filePath = getLoadedFilePath(file) } return if (filePath != null) { - try { + loadedImageCache[filePath] ?: try { val data = if (file?.fileSource?.cryptoArgs != null) readCryptoFile(filePath, file.fileSource.cryptoArgs) else File(filePath).readBytes() val bitmap = getBitmapFromByteArray(data, false) - if (bitmap != null) bitmap to data else null + if (bitmap != null) (bitmap to data).also { loadedImageCache[filePath] = it } else null } catch (e: Exception) { Log.e(TAG, "Unable to read crypto file: " + e.stackTraceToString()) null diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index 60ff535e88..1e7bda37c4 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -40,6 +40,7 @@ compose { } mainClass = "chat.simplex.desktop.MainKt" nativeDistributions { + copyright = "(c) 2020-2026 SimpleX Chat" // For debugging via VisualVM if (debugJava) { modules("jdk.zipfs", "jdk.unsupported", "jdk.management.agent") diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 38ef04daaa..1354ce0cf3 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.5-beta.3 -android.version_code=331 +android.version_name=6.5-beta.5 +android.version_code=335 android.bundle=false -desktop.version_name=6.5-beta.3 -desktop.version_code=128 +desktop.version_name=6.5-beta.5 +desktop.version_code=131 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 diff --git a/apps/multiplatform/product/README.md b/apps/multiplatform/product/README.md new file mode 100644 index 0000000000..173def8ae7 --- /dev/null +++ b/apps/multiplatform/product/README.md @@ -0,0 +1,396 @@ +# SimpleX Chat Android & Desktop -- Product Overview + +> SimpleX Chat multiplatform product specification (Android + Desktop). Bidirectional code links: product docs reference source files, source files reference product docs. +> +> **Related spec:** [spec/README.md](../spec/README.md) | [spec/architecture.md](../spec/architecture.md) + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Vision](#vision) +3. [Target Users](#target-users) +4. [Capability Map](#capability-map) +5. [Navigation Map](#navigation-map) +6. [Related Specifications](#related-specifications) + +## Executive Summary + +SimpleX Chat is the first messaging platform with no user identifiers of any kind -- not even random numbers. It provides end-to-end encrypted messaging (with optional post-quantum cryptography), audio/video calls, file sharing, and group communication through a fully decentralized architecture where users control their own SMP relay servers. + +The Android and Desktop apps share a single **Kotlin Multiplatform + Compose Multiplatform** codebase. Common UI and business logic lives in a shared `common/` module, while platform-specific behavior (notifications, audio, video playback, file system access, call management) is abstracted through the Kotlin `expect`/`actual` pattern and a runtime `PlatformInterface` delegate. The Haskell core library is loaded via **JNI** (`external fun` declarations in `Core.kt`), exposing the full SimpleX Chat API (message send/receive, encryption, migration, file handling) through native FFI. + +Key platform differences: + +- **Android** uses a 2-column layout (`AndroidScreen`): chat list slides to chat view. Background messaging is handled by `SimplexService` (foreground service) + `MessagesFetcherWorker` (WorkManager periodic fetch). Calls use a dedicated `CallService` + `CallActivity`. +- **Desktop** uses a 3-column layout (`DesktopScreen`): chat list (start) | chat view (center) | detail panel (`ModalManager.end`). It includes `AppUpdater` for in-app update checking, `StoreWindowState` for window geometry persistence, and VLC-based video playback. Calls use browser-based WebRTC rendered inline. + +--- + +## Vision + +SimpleX Chat is the first messaging platform that has no user identifiers -- not even random numbers. It uses double-ratchet end-to-end encryption with optional post-quantum cryptography. The system is fully decentralized with user-controlled SMP relay servers. + +The protocol design ensures that no server or network observer can determine who communicates with whom. Each conversation uses separate unidirectional messaging queues on potentially different servers, and there is no shared identifier between the sender and receiver queues. + +--- + +## Target Users + +- **Privacy-conscious individuals** wanting secure messaging without phone-number or email-based identity +- **Groups and communities** needing encrypted group communication with role-based access control +- **Users avoiding identity linkage** who want to communicate without any persistent user identifier +- **Organizations** needing self-hosted messaging infrastructure with full control over relay servers +- **Desktop users** wanting a native desktop client with the same privacy guarantees as the mobile app + +--- + +## Capability Map + +All source paths below are relative to `apps/multiplatform/`. The common source root is `common/src/commonMain/kotlin/chat/simplex/common/`. + +### 1. Messaging + +Core message composition, delivery, and interaction features. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Text with markdown | Rich text formatting with SimpleX markdown syntax | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt` | +| Images | Compressed inline images with full-screen gallery | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt` | +| Video | Video message recording and playback | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt` | +| Voice messages | Audio recording and playback (5min / 510KB limit) | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt` | +| File sharing | Files up to 1GB via XFTP protocol | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt` | +| Link previews | OpenGraph metadata extraction and display | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt` | +| Message reactions | Emoji reactions on sent/received messages | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt` | +| Message editing | Edit previously sent messages | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt` | +| Message deletion | Broadcast delete (for recipient) or internal-only delete | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt` | +| Timed messages | Self-destructing messages with configurable TTL | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt` | +| Quoted replies | Reply to specific messages with quote context | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt` | +| Forwarding | Forward messages between chats | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt` | +| Search | Full-text search within conversations | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt` | +| Message reports | Report messages to group moderators | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt` | +| Send message bar | Composable message input with attachments, voice, send button | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt` | + +### 2. Contacts + +Establishing, managing, and verifying contacts. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Add via SimpleX address | Connect using a SimpleX contact address | `common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt` | +| Add via QR code | Scan QR code to establish connection | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt` | +| Contact requests | Accept or reject incoming contact requests | `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt` | +| Local aliases | Set private display names for contacts | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt` | +| Contact verification | Compare security codes out-of-band | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt` | +| Blocking | Block contacts from sending messages | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt` | +| Incognito mode | Per-contact random profile generation | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt` | +| Bot detection | Identify automated/bot contacts | `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` | +| Contact list | Dedicated contact browsing view | `common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt` | + +### 3. Groups + +Multi-party encrypted conversations with role-based management. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Create groups | Create new group with initial members | `common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt` | +| Invite members | Invite by individual contact or link | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt` | +| Member roles | Owner, admin, moderator, member, observer | `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` | +| Member admission | Queue-based admission with review workflow | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt` | +| Group links | Shareable invite links for groups | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt` | +| Business chat mode | Structured business communication groups | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt` | +| Content moderation | Member reports and moderator actions | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt` | +| Group preferences | Configure group-level feature settings | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt` | +| Member direct contacts | Establish direct chats from group membership | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt` | +| Group mentions | @-mention members in group messages | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt` | +| Welcome message | Custom welcome message for new group members | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt` | +| Group profile | Edit group name, image, description | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt` | +| Member support chat | Scoped support threads between members and admins | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt` | + +### 4. Calling + +End-to-end encrypted audio and video communication. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| E2E encrypted calls | Audio/video calls via WebRTC with E2E encryption | `common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt` | +| Call manager | Call state machine and lifecycle management | `common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt` | +| Call history | Call events displayed as chat items | `common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt` | +| Incoming call view | Dedicated UI for incoming call notifications | `common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt` | +| Android CallService | Foreground service for active calls on Android | `android/src/main/java/chat/simplex/app/CallService.kt` | +| Android CallActivity | Dedicated Activity for call UI on Android | `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` | +| Desktop inline calls | Browser-based WebRTC rendered inline in desktop window | `common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt` | + +### 5. Privacy & Security + +Encryption, authentication, and privacy controls. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| E2E encryption | Double-ratchet encryption for all messages | `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | +| Post-quantum encryption | Optional PQ key exchange for direct chats | `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` | +| Local authentication | Biometric (fingerprint/face) or app passcode lock | `common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt` | +| Passcode entry | Custom numeric/alphanumeric passcode UI | `common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt` | +| Hidden profiles | Password-protected profiles invisible in UI | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt` | +| Database encryption | AES encryption of local SQLite database | `common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt` | +| Screen privacy | Blur/hide app content when in app switcher | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt` | +| Encrypted file storage | Local files encrypted at rest | `common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt` | +| Delivery receipts control | Toggle delivery/read receipts per contact/group | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt` | +| App lock | Automatic lock on background/timeout with configurable delay | `common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt` | + +### 6. User Management + +Multiple profiles and identity management. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Multiple profiles | Multiple user profiles within one app | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt` | +| Active user switching | Switch between profiles via user picker | `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt` | +| Incognito contacts | Per-contact random identities | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt` | +| Profile sharing | Share profile via contact address link | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt` | +| User muting | Mute notifications for specific profiles | `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt` | +| User profile editing | Edit display name and profile image | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt` | + +### 7. Network + +Server configuration, proxy support, and connectivity. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Custom SMP servers | Configure personal SMP relay servers | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt` | +| Custom XFTP servers | Configure personal XFTP file servers | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt` | +| Tor/onion support | Route traffic through Tor .onion addresses | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | +| SOCKS5 proxy | Route connections through SOCKS5 proxy | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | +| Custom ICE servers | Configure WebRTC ICE/TURN servers | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt` | +| Network timeouts | Configure connection timeout parameters | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | +| Server operators | Configure and manage SMP/XFTP server operators | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt` | +| Server status | View aggregate server connectivity status | `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt` | +| Network & servers hub | Central network configuration entry point | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt` | + +### 8. Customization + +Visual appearance and UI preferences. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Themes | Light, dark, SimpleX, black, and custom themes | `common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt` | +| Wallpapers | Preset and custom chat wallpapers | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt` | +| Chat bubble styling | Customize message bubble appearance | `common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt` | +| One-handed UI mode | Compact layout for single-hand use (Android) | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt` | +| Language selection | In-app language override | `common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt` | +| Theme mode editor | Interactive theme color and mode customization | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt` | + +### 9. Data Management + +Import, export, encryption, and storage management. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| Export/import profiles | Full database export and import | `common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt` | +| Database encryption | Encrypt/decrypt local database with passphrase | `common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt` | +| Local file encryption | Encrypt stored media and attachments | `common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt` | +| Database error handling | Recovery UI for database migration failures | `common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt` | +| Device-to-device migration | Migrate full profile between devices | `common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt` | +| Receive migration | Accept incoming device migration transfer | `common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt` | +| Database utilities | Key storage, password management, helper functions | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` | + +### 10. Desktop Features + +Desktop-specific functionality not present on Android. + +| Feature | Description | Key Source (Kotlin) | +|---------|-------------|---------------------| +| 3-column layout | Start (chat list) / center (chat) / end (detail) panels | `common/src/commonMain/kotlin/chat/simplex/common/App.kt` (`DesktopScreen`) | +| ModalManager.end | Third-column detail panel for settings/info views | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt` (`ModalManager`) | +| App update checker | In-app notification for available updates | `common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt` | +| Window state persistence | Save/restore window position and dimensions | `common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt` | +| VLC video playback | Desktop video playback via VLC native libraries | `common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt` | +| Desktop app entry | Main function, Haskell init, VLC loading | `desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt` | +| Desktop notification manager | Platform-native desktop notifications | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt` | +| Connect mobile device | Pair desktop with a mobile device for remote access | `common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt` | +| Desktop platform abstraction | Desktop-specific PlatformInterface implementation | `common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt` | +| Desktop app shell | Compose Desktop window, theming, lifecycle | `common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt` | + +--- + +## Navigation Map + +### Android Navigation (2-column slide) + +``` +Onboarding + views/onboarding/SimpleXInfo.kt + -> SimpleXInfo -> CreateFirstProfile -> SetupDatabasePassphrase + -> ChooseServerOperators -> SetNotificationsMode + -> ChatListView (home) + +ChatListView (home) + views/chatlist/ChatListView.kt + -> ChatView .................. (tap conversation row, slides in) + -> NewChatSheet .............. (+ FAB button) + -> SettingsView .............. (gear icon) + -> UserPicker ................ (avatar tap) + -> TagListView ............... (tag filter bar) + -> ServersSummaryView ........ (server status indicator) + -> ShareListView ............. (share intent from external apps) + -> ChatHelpView .............. (empty state help) + +ChatView + views/chat/ChatView.kt + -> ChatInfoView .............. (contact name tap, direct chat) + -> GroupChatInfoView ......... (group name tap, group chat) + -> ActiveCallView ............ (call button, launches CallActivity) + -> ComposeView ............... (message input area) + -> ChatItemInfoView .......... (long press -> info) + -> MemberSupportChatView ..... (member support thread) + -> ScanCodeView .............. (scan QR) + -> CommandsMenuView .......... (/ commands) + +ChatInfoView + views/chat/ChatInfoView.kt + -> ContactPreferences ........ (preferences) + -> VerifyCodeView ............ (verify security code) + +GroupChatInfoView + views/chat/group/GroupChatInfoView.kt + -> GroupProfileView .......... (edit profile) + -> AddGroupMembersView ....... (invite members) + -> GroupLinkView ............. (manage group link) + -> MemberAdmission ........... (admission settings) + -> GroupPreferences .......... (group feature settings) + -> GroupMemberInfoView ....... (tap member) + -> WelcomeMessageView ........ (welcome message) + -> GroupReportsView .......... (view reports) + +NewChatSheet + views/newchat/NewChatSheet.kt + -> NewChatView ............... (QR scanner / paste link) + -> AddGroupView .............. (create group) + -> UserAddressView ........... (create SimpleX address) + +SettingsView + views/usersettings/SettingsView.kt + -> AppearanceView ............ (themes, wallpapers, UI) + -> NetworkAndServers ......... (SMP/XFTP/proxy config) + -> PrivacySettings ........... (privacy toggles) + -> NotificationsSettingsView . (notification mode) + -> DatabaseView .............. (export/import/encrypt) + -> CallSettings .............. (call preferences) + -> VersionInfoView ........... (about/version) + -> DeveloperView ............. (developer options) + -> HelpView .................. (help & support) + +UserPicker + views/chatlist/UserPicker.kt + -> UserProfilesView .......... (manage all profiles) + -> UserAddressView ........... (SimpleX address) + -> Preferences ............... (user preferences) + -> SettingsView .............. (app settings) + -> ConnectDesktopView ........ (pair with desktop) +``` + +### Desktop Navigation (3-column panels) + +``` ++---------------------------+----------------------------------+----------------------------+ +| START PANEL | CENTER PANEL | END PANEL | +| (DEFAULT_START_MODAL_ | (flexible width, min | (DEFAULT_END_MODAL_ | +| WIDTH) | DEFAULT_MIN_CENTER_MODAL_ | WIDTH) | +| | WIDTH) | | ++---------------------------+----------------------------------+----------------------------+ +| | | | +| ChatListView | ChatView | ChatInfoView | +| - chat rows | - message list | GroupChatInfoView | +| - search | - ComposeView | GroupMemberInfoView | +| - tag filters | - media viewer | ContactPreferences | +| - server status | | GroupPreferences | +| | OR (when no chat selected): | GroupProfileView | +| UserPicker (overlay) | "No selected chat" | AddGroupMembersView | +| - profile switcher | | MemberAdmission | +| - quick settings | OR (when modal open): | VerifyCodeView | +| | ModalManager.center content | SettingsView subtabs | +| ModalManager.start | (settings, new chat, etc.) | | +| - secondary modals | | ModalManager.end | +| | | - detail modals | ++---------------------------+----------------------------------+----------------------------+ + +ModalManager Placement (Desktop): + - ModalManager.start -> left panel overlay (settings subviews) + - ModalManager.center -> center panel (replaces chat, used when chatId is null) + - ModalManager.end -> right panel (detail/info views) + - ModalManager.fullscreen -> full window overlay (onboarding, auth, call) + +On Android, all ModalManager instances (start/center/end/fullscreen) collapse to a +single shared ModalManager that presents modals as full-screen overlays. + +Desktop-only navigation targets: + ConnectMobileView ......... (pair with mobile device) + AppUpdater notice ......... (update available notification) + Floating terminal ......... (developer console) + ActiveCallView ............ (inline WebRTC call, not separate Activity) +``` + +--- + +## Platform Abstraction + +The codebase uses two mechanisms for platform-specific behavior: + +### 1. `expect`/`actual` Declarations + +Kotlin Multiplatform `expect` declarations in `common/src/commonMain/kotlin/chat/simplex/common/platform/` with corresponding `actual` implementations in: +- `common/src/androidMain/kotlin/chat/simplex/common/platform/*.android.kt` +- `common/src/desktopMain/kotlin/chat/simplex/common/platform/*.desktop.kt` + +Key `expect`/`actual` abstractions: `appPlatform`, `BackHandler`, `VideoPlayer`, `AudioPlayer`, `RecorderNative`, `NtfManager`, `showToast`, `getKeyboardState`, `PlatformTextField`, image processing, file sharing, and more. + +### 2. Runtime `PlatformInterface` + +Defined in `common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt`, this interface provides platform-specific callbacks that cannot use `expect`/`actual` (because `android/` module code cannot be called from `common/androidMain/`). The `platform` variable is reassigned at app startup: +- **Android:** `SimplexApp` sets `platform` to an implementation with `CallService`, notification channels, orientation locking, status bar theming, and PiP support. +- **Desktop:** `Main.kt` sets `platform` to an implementation with `desktopShowAppUpdateNotice()`. + +### 3. Haskell Core (JNI/FFI) + +Native FFI bindings are declared in `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` as `external fun` declarations. These include: `chatMigrateInit`, `chatSendCmdRetry`, `chatRecvMsg`, `chatParseMarkdown`, `chatPasswordHash`, `chatWriteFile`, `chatReadFile`, `chatEncryptFile`, `chatDecryptFile`, and more. The native library (`libapp-lib`) is loaded at startup from platform-specific resource directories. + +--- + +## Background Messaging (Android) + +Android has no equivalent to iOS NSE (Notification Service Extension). Instead, it uses: + +- **`SimplexService`** (`android/src/main/java/chat/simplex/app/SimplexService.kt`) -- A foreground service that keeps the Haskell core running to receive messages in real-time. Displays a persistent notification while active. +- **`MessagesFetcherWorker`** (`android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt`) -- A WorkManager-based periodic task that wakes the app at configurable intervals to fetch messages when the foreground service is not running (battery-optimized mode). +- **Notification modes:** Instant (foreground service always running), Periodic (WorkManager fetch every N minutes), Off. + +--- + +## Related Specifications + +### Product Layer (this directory) + +- [concepts.md](concepts.md) -- Feature concept index with bidirectional code links +- [glossary.md](glossary.md) -- Terminology definitions +- [rules.md](rules.md) -- Business rules and constraints +- [gaps.md](gaps.md) -- Known documentation gaps +- Views: [chat-list](views/chat-list.md), [chat](views/chat.md), [new-chat](views/new-chat.md), [settings](views/settings.md), [call](views/call.md), [contact-info](views/contact-info.md), [group-info](views/group-info.md), [onboarding](views/onboarding.md), [user-profiles](views/user-profiles.md) +- Flows: [messaging](flows/messaging.md), [calling](flows/calling.md), [onboarding](flows/onboarding.md), [group-lifecycle](flows/group-lifecycle.md), [connection](flows/connection.md), [file-transfer](flows/file-transfer.md) + +### Spec Layer + +- [spec/README.md](../spec/README.md) -- Technical specification overview +- [spec/architecture.md](../spec/architecture.md) -- JNI bridge, startup, lifecycle +- [spec/state.md](../spec/state.md) -- ChatModel, ChatsContext, Chat, AppPreferences +- [spec/api.md](../spec/api.md) -- Command/response protocol (CC, CR, ChatError) +- [spec/database.md](../spec/database.md) -- Migration, encryption, export/import +- Client: [navigation](../spec/client/navigation.md), [chat-list](../spec/client/chat-list.md), [chat-view](../spec/client/chat-view.md), [compose](../spec/client/compose.md) +- Services: [calls](../spec/services/calls.md), [theme](../spec/services/theme.md), [files](../spec/services/files.md), [notifications](../spec/services/notifications.md) + +### Source Entry Points + +- Haskell core: `../../src/Simplex/Chat/Controller.hs`, `../../src/Simplex/Chat/Types.hs` +- Kotlin model: `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` +- Kotlin API bridge: `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` +- Kotlin FFI: `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` +- Android entry: `android/src/main/java/chat/simplex/app/SimplexApp.kt`, `MainActivity.kt` +- Desktop entry: `desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt` diff --git a/apps/multiplatform/product/concepts.md b/apps/multiplatform/product/concepts.md new file mode 100644 index 0000000000..da33bf11d7 --- /dev/null +++ b/apps/multiplatform/product/concepts.md @@ -0,0 +1,120 @@ +# SimpleX Chat Android & Desktop -- Concept Index + +> SimpleX Chat multiplatform concept index. Maps every product concept to its documentation and source code with bidirectional links. +> +> **Related spec:** [spec/README.md](../spec/README.md) | [spec/architecture.md](../spec/architecture.md) + +## Table of Contents + +1. [Feature Concepts](#section-1-feature-concepts) +2. [Entity Index](#section-2-entity-index) + +## Executive Summary + +This document provides a structured mapping between product-level concepts, their documentation, and their implementation in both the Kotlin multiplatform layer and the Haskell core library. All Kotlin source paths are relative to `apps/multiplatform/`. Haskell paths use `../../src/` prefix (relative to `apps/multiplatform/`). The common source root abbreviation used below is `common/src/commonMain/kotlin/chat/simplex/common/`. + +--- + +## Section 1: Feature Concepts + +| # | Concept | Product Docs | Spec Docs | Source Files (Kotlin) | Source Files (Haskell) | +|---|---------|-------------|-----------|----------------------|----------------------| +| PC1 | Chat List | [README.md](README.md) (Navigation Map) | [spec/client/chat-list.md](../spec/client/chat-list.md) | `common/.../views/chatlist/ChatListView.kt`, `ChatListNavLinkView.kt`, `ChatPreviewView.kt` | `Controller.hs` (`APIGetChats`) | +| PC2 | Direct Chat | [README.md](README.md) (Messaging) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/chat/ChatView.kt`, `ChatInfoView.kt` | `Types.hs` (`Contact`), `Messages.hs` | +| PC3 | Group Chat | [README.md](README.md) (Groups) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/chat/ChatView.kt`, `group/GroupChatInfoView.kt` | `Types.hs` (`GroupInfo`, `GroupMember`) | +| PC4 | Message Composition | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/ComposeView.kt`, `SendMsgView.kt`, `ComposeVoiceView.kt`, `ComposeImageView.kt`, `ComposeFileView.kt` | `Controller.hs` (`APISendMessages`) | +| PC5 | Message Reactions | [README.md](README.md) (Messaging) | [spec/api.md](../spec/api.md) | `common/.../views/chat/ChatItemView.kt` (ChatItemReactions composable) | `Controller.hs` (`APIChatItemReaction`) | +| PC6 | Message Editing | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/ComposeView.kt`, `ChatItemInfoView.kt` | `Controller.hs` (`APIUpdateChatItem`) | +| PC7 | Message Deletion | [README.md](README.md) (Messaging) | [spec/api.md](../spec/api.md) | `common/.../views/chat/item/MarkedDeletedItemView.kt`, `DeletedItemView.kt` | `Controller.hs` (`APIDeleteChatItem`) | +| PC8 | Timed Messages | [README.md](README.md) (Messaging) | [spec/api.md](../spec/api.md) | `common/.../views/chat/item/CIChatFeatureView.kt` | `Types/Preferences.hs` (`TimedMessagesPreference`) | +| PC9 | Voice Messages | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/item/CIVoiceView.kt`, `ComposeVoiceView.kt`, `platform/RecAndPlay.kt` | `Protocol.hs` (`MCVoice`) | +| PC10 | File Transfer | [README.md](README.md) (Messaging, Data Management) | [spec/services/files.md](../spec/services/files.md) | `common/.../views/chat/item/CIFileView.kt`, `platform/Files.kt` | `Files.hs`, `Store/Files.hs` | +| PC11 | Link Previews | [README.md](README.md) (Messaging) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/helpers/LinkPreviews.kt` | `Protocol.hs` (`MCLink`) | +| PC12 | Contact Connection | [README.md](README.md) (Contacts) | [spec/api.md](../spec/api.md) | `common/.../views/newchat/NewChatView.kt`, `QRCode.kt`, `QRCodeScanner.kt`, `ConnectPlan.kt` | `Controller.hs` (`APIConnect`, `APIAddContact`) | +| PC13 | Contact Verification | [README.md](README.md) (Contacts) | [spec/api.md](../spec/api.md) | `common/.../views/chat/VerifyCodeView.kt` | `Controller.hs` (`APIVerifyContact`) | +| PC14 | Group Management | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../views/newchat/AddGroupView.kt`, `group/GroupChatInfoView.kt`, `group/GroupProfileView.kt` | `Controller.hs` (`APINewGroup`), `Store/Groups.hs` | +| PC15 | Group Links | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../views/chat/group/GroupLinkView.kt` | `Controller.hs` (`APICreateGroupLink`) | +| PC16 | Member Roles | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../model/ChatModel.kt`, `group/GroupMemberInfoView.kt` | `Types/Shared.hs` (`GroupMemberRole`) | +| PC17 | Audio/Video Calls | [README.md](README.md) (Calling) | [spec/services/calls.md](../spec/services/calls.md) | `common/.../views/call/CallView.kt`, `CallManager.kt`, `WebRTC.kt`, `android/.../CallService.kt`, `android/.../views/call/CallActivity.kt` | `Call.hs` (`RcvCallInvitation`, `CallType`) | +| PC18 | Notifications | [README.md](README.md) (Background Messaging) | [spec/services/notifications.md](../spec/services/notifications.md) | `common/.../platform/NtfManager.kt`, `Notifications.kt`, `android/.../SimplexService.kt`, `android/.../MessagesFetcherWorker.kt`, `common/.../views/usersettings/NotificationsSettingsView.kt` | `Controller.hs` | +| PC19 | User Profiles | [README.md](README.md) (User Management) | [spec/state.md](../spec/state.md) | `common/.../views/usersettings/UserProfilesView.kt`, `UserProfileView.kt`, `views/chatlist/UserPicker.kt` | `Types.hs` (`User`), `Store/Profiles.hs` | +| PC20 | Incognito Mode | [README.md](README.md) (Contacts) | [spec/api.md](../spec/api.md) | `common/.../views/usersettings/IncognitoView.kt` | `ProfileGenerator.hs`, `Types.hs` | +| PC21 | Hidden Profiles | [README.md](README.md) (Privacy & Security) | [spec/api.md](../spec/api.md) | `common/.../views/usersettings/HiddenProfileView.kt` | `Controller.hs` (`APIHideUser`, `APIUnhideUser`) | +| PC22 | Local Authentication | [README.md](README.md) (Privacy & Security) | [spec/architecture.md](../spec/architecture.md) | `common/.../views/localauth/LocalAuthView.kt`, `PasscodeView.kt`, `SetAppPasscodeView.kt`, `PasswordEntry.kt`, `AppLock.kt` | N/A (client-only) | +| PC23 | Database Encryption | [README.md](README.md) (Data Management) | [spec/database.md](../spec/database.md) | `common/.../views/database/DatabaseEncryptionView.kt`, `DatabaseView.kt`, `views/helpers/DatabaseUtils.kt` | `Controller.hs` (`APIExportArchive`) | +| PC24 | Theme System | [README.md](README.md) (Customization) | [spec/services/theme.md](../spec/services/theme.md) | `common/.../ui/theme/ThemeManager.kt`, `Theme.kt`, `Color.kt`, `Type.kt`, `Shape.kt` | `Types/UITheme.hs` | +| PC25 | Network Configuration | [README.md](README.md) (Network) | [spec/architecture.md](../spec/architecture.md) | `common/.../views/usersettings/networkAndServers/NetworkAndServers.kt`, `ProtocolServersView.kt`, `AdvancedNetworkSettings.kt`, `OperatorView.kt` | `Controller.hs` (`APISetNetworkConfig`) | +| PC26 | Device Migration | [README.md](README.md) (Data Management) | [spec/database.md](../spec/database.md) | `common/.../views/migration/MigrateFromDevice.kt`, `MigrateToDevice.kt` | `Archive.hs` | +| PC27 | Remote Desktop | [README.md](README.md) (Desktop Features) | [spec/architecture.md](../spec/architecture.md) | `common/.../views/remote/ConnectDesktopView.kt`, `ConnectMobileView.kt` | `Remote.hs`, `Remote/Types.hs` | +| PC28 | Chat Tags | [README.md](README.md) (Navigation Map) | [spec/state.md](../spec/state.md) | `common/.../views/chatlist/TagListView.kt`, `ChatListView.kt` | `Types.hs` (`ChatTag`), `Controller.hs` | +| PC29 | User Address | [README.md](README.md) (Contacts, User Management) | [spec/api.md](../spec/api.md) | `common/.../views/usersettings/UserAddressView.kt`, `UserAddressLearnMore.kt` | `Controller.hs` (`APICreateMyAddress`) | +| PC30 | Member Support Chat | [README.md](README.md) (Groups) | [spec/api.md](../spec/api.md) | `common/.../views/chat/group/MemberSupportView.kt`, `MemberSupportChatView.kt`, `MemberAdmission.kt` | `Messages.hs` (`GroupChatScope`), `Controller.hs` | + +**Legend for abbreviated paths:** +- `common/.../` expands to `common/src/commonMain/kotlin/chat/simplex/common/` +- `android/.../` expands to `android/src/main/java/chat/simplex/app/` +- Haskell files are in `../../src/Simplex/Chat/` (relative to `apps/multiplatform/`) + +--- + +## Section 2: Entity Index + +Core data entities, their storage, and the operations that manage their lifecycle. + +| Entity | DB Table (Haskell) | Created By | Read By | Mutated By | Deleted By | +|--------|-------------------|------------|---------|------------|------------| +| **User** | `users` | `CreateActiveUser` in `Controller.hs` | `ListUsers`, `APISetActiveUser` in `Controller.hs` | `APISetActiveUser`, `APIHideUser`, `APIUnhideUser`, `APIMuteUser`, `APIUpdateProfile` in `Controller.hs` | `APIDeleteUser` in `Controller.hs`; `Store/Profiles.hs` | +| **Contact** | `contacts`, `contact_profiles` | `APIAddContact`, `APIConnect` in `Controller.hs` | `APIGetChat` in `Controller.hs`; `Store/Direct.hs` (`getContact`) | `APISetContactAlias`, `APISetConnectionAlias` in `Controller.hs`; `Store/Direct.hs` | `APIDeleteChat` in `Controller.hs`; `Store/Direct.hs` (`deleteContact`) | +| **GroupInfo** | `groups`, `group_profiles` | `APINewGroup` in `Controller.hs`; `Store/Groups.hs` (`createNewGroup`) | `APIGetChat`, `APIGroupInfo` in `Controller.hs`; `Store/Groups.hs` | `APIUpdateGroupProfile` in `Controller.hs`; `Store/Groups.hs` (`updateGroupProfile`) | `APIDeleteChat` in `Controller.hs`; `Store/Groups.hs` (`deleteGroup`) | +| **GroupMember** | `group_members`, `contact_profiles` | `APIAddMember`, `APIJoinGroup` in `Controller.hs`; `Store/Groups.hs` (`createNewGroupMember`) | `APIListMembers` in `Controller.hs`; `Store/Groups.hs` (`getGroupMembers`) | `APIMembersRole` in `Controller.hs`; `Store/Groups.hs` (`updateGroupMemberRole`) | `APIRemoveMembers` in `Controller.hs`; `Store/Groups.hs` (`deleteGroupMember`) | +| **ChatItem** | `chat_items`, `chat_item_versions` | `APISendMessages` in `Controller.hs`; `Store/Messages.hs` (`createNewChatItem`) | `APIGetChat`, `APIGetChatItems` in `Controller.hs`; `Store/Messages.hs` (`getChatItems`) | `APIUpdateChatItem`, `APIChatItemReaction` in `Controller.hs`; `Store/Messages.hs` (`updateChatItem`) | `APIDeleteChatItem` in `Controller.hs`; `Store/Messages.hs` (`deleteChatItem`) | +| **Connection** | `connections` | `createConnection` via SMP agent; `Store/Connections.hs` | `Store/Connections.hs` (`getConnectionEntity`) | `Store/Connections.hs` (`updateConnectionStatus`) | `Store/Connections.hs` (`deleteConnection`) | +| **FileTransfer** | `files`, `snd_files`, `rcv_files`, `xftp_file_descriptions` | `APISendMessages` (with file), `ReceiveFile` in `Controller.hs`; `Store/Files.hs` | `Store/Files.hs` (`getFileTransfer`) | `Store/Files.hs` (`updateFileStatus`, `updateFileProgress`) | `Store/Files.hs` (`deleteFileTransfer`) | +| **GroupLink** | `user_contact_links` | `APICreateGroupLink` in `Controller.hs`; `Store/Groups.hs` | `APIGetGroupLink` in `Controller.hs`; `Store/Groups.hs` | N/A (recreated on change) | `APIDeleteGroupLink` in `Controller.hs`; `Store/Groups.hs` | +| **ChatTag** | `chat_tags`, `chat_tags_chats` | `APICreateChatTag` in `Controller.hs` | `APIGetChats` in `Controller.hs` | `APIUpdateChatTag`, `APISetChatTags` in `Controller.hs` | `APIDeleteChatTag` in `Controller.hs` | +| **RcvCallInvitation** | In-memory (not persisted) | Received via `XCallInv` message in `Library/Subscriber.hs`; stored in `ChatModel.activeCallInvitation` | `CallManager.kt`, `IncomingCallAlertView.kt` | Updated on call accept/reject in `CallManager.kt` | Removed on call end/reject; `Controller.hs` | + +--- + +## Platform-Specific Source Index + +Key files that exist only on one platform, grouped by concern. + +### Android-Only + +| File | Purpose | +|------|---------| +| `android/src/main/java/chat/simplex/app/SimplexApp.kt` | Application subclass, PlatformInterface setup, Haskell init | +| `android/src/main/java/chat/simplex/app/MainActivity.kt` | Main Activity, deep link handling, lifecycle | +| `android/src/main/java/chat/simplex/app/SimplexService.kt` | Foreground service for persistent messaging | +| `android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt` | WorkManager periodic message fetch | +| `android/src/main/java/chat/simplex/app/CallService.kt` | Foreground service for active calls | +| `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` | Dedicated Activity for call UI | +| `android/src/main/java/chat/simplex/app/model/NtfManager.android.kt` | Android notification channels and manager | +| `common/src/androidMain/kotlin/chat/simplex/common/platform/*.android.kt` | All `actual` implementations for Android | + +### Desktop-Only + +| File | Purpose | +|------|---------| +| `desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt` | JVM entry point, Haskell/VLC init, PlatformInterface setup | +| `common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt` | Compose Desktop window creation and lifecycle | +| `common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt` | Window position/size persistence | +| `common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt` | In-app update checker | +| `common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt` | VLC-based video detection | +| `common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt` | VLC video player implementation | +| `common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt` | Desktop platform detection (Linux/macOS/Windows) | +| `common/src/desktopMain/kotlin/chat/simplex/common/platform/*.desktop.kt` | All `actual` implementations for Desktop | + +--- + +## Cross-References + +- Product overview: [README.md](README.md) +- Haskell core controller: `../../src/Simplex/Chat/Controller.hs` +- Haskell core types: `../../src/Simplex/Chat/Types.hs` +- Haskell store layer: `../../src/Simplex/Chat/Store/` (`Direct.hs`, `Groups.hs`, `Messages.hs`, `Files.hs`, `Profiles.hs`, `Connections.hs`) +- Kotlin model: `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` +- Kotlin API bridge: `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` +- Kotlin FFI layer: `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` +- Platform abstraction: `common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt` (`PlatformInterface`) diff --git a/apps/multiplatform/product/flows/calling.md b/apps/multiplatform/product/flows/calling.md new file mode 100644 index 0000000000..fae7f42031 --- /dev/null +++ b/apps/multiplatform/product/flows/calling.md @@ -0,0 +1,220 @@ +# Calling Flow + +> **Related spec:** [spec/services/calls.md](../../spec/services/calls.md) + +## Overview + +SimpleX Chat supports audio and video calls using WebRTC, with signaling delivered over the existing SMP messaging channels. Calls are end-to-end encrypted with an additional shared key layer on top of WebRTC's SRTP encryption. + +The architecture differs by platform: +- **Android**: Calls run in a dedicated `CallActivity` (separate from `MainActivity`) with a `WebView` hosting the WebRTC JavaScript. A foreground `CallService` keeps the process alive and shows a persistent notification. +- **Desktop**: Calls open the system browser pointed at a local NanoHTTPD/NanoWSD embedded server on `localhost:50395`, which serves the WebRTC HTML/JS and communicates with the app via WebSocket. + +Both platforms share a common signaling flow through the Haskell core API. + +## Prerequisites + +- Both parties must have an established direct contact connection. +- Microphone permission is required; camera permission is required for video calls. +- On Android, the `CallOnLockScreen` preference controls lock-screen call behavior: `DISABLE`, `SHOW`, or `ACCEPT`. + +--- + +## 1. Outgoing Call (Caller Side) + +### 1.1 Initiate Call + +1. User taps the audio or video call button in `ChatView`. +2. `startChatCall(remoteHostId, chatInfo, media)` is called (in `ChatView.kt`). +3. A `Call` object is created with `callState = CallState.WaitCapabilities`: + ```kotlin + Call( + remoteHostId = remoteHostId, + contact = contact, + callUUID = null, + callState = CallState.WaitCapabilities, + initialCallType = media, // Audio or Video + userProfile = profile, + androidCallState = platform.androidCreateActiveCallState() + ) + ``` +4. `ChatModel.activeCall` is set and `ChatModel.showCallView` is set to `true`. +5. A `WCallCommand.Capabilities(media)` command is added to `ChatModel.callCommand`. + +### 1.2 WebRTC Capabilities Response + +1. The WebRTC engine (WebView on Android, browser on Desktop) receives the `Capabilities` command. +2. It responds with `WCallResponse.Capabilities(capabilities)` containing encryption support info. +3. The app calls `ChatController.apiSendCallInvitation(rh, contact, callType)` to send the invitation via SMP. +4. Call state transitions to `CallState.InvitationSent`. +5. A connecting sound starts playing via `CallSoundsPlayer.startConnectingCallSound`. + +### 1.3 Offer Exchange + +1. When the callee accepts, the WebRTC engine generates an offer. +2. `WCallResponse.Offer(offer, iceCandidates, capabilities)` is received. +3. `ChatController.apiSendCallOffer(rh, contact, rtcSession, rtcIceCandidates, media, capabilities)` sends it. +4. Call state transitions to `CallState.OfferSent`. + +### 1.4 Answer and Connection + +1. The callee's answer arrives via SMP as a chat event. +2. The app dispatches `WCallCommand.Answer(answer, iceCandidates)` to the WebRTC engine. +3. Call state transitions to `CallState.Negotiated`, then to `CallState.Connected` once the ICE connection succeeds. +4. `Call.connectedAt` is set to the current timestamp. + +--- + +## 2. Incoming Call (Callee Side) + +### 2.1 Receive Invitation + +1. An incoming call event arrives from the core as `CR.CallInvitation`. +2. `CallManager.reportNewIncomingCall(invitation)` is called. +3. A `RcvCallInvitation` is stored in `ChatModel.callInvitations` keyed by contact ID. +4. If the invitation is recent (within 3 minutes), a system notification is shown and `ChatModel.activeCallInvitation` is set. +5. On Android, `CallActivity` may be launched on the lock screen if `callOnLockScreen` is `SHOW` or `ACCEPT`. + +### 2.2 Accept Call + +1. User taps "Accept" on the `IncomingCallAlertView` or lock-screen alert. +2. `CallManager.acceptIncomingCall(invitation)` is called. +3. If another call is active, it is ended first (with `switchingCall` flag set). +4. A new `Call` is created with `callState = CallState.InvitationAccepted`. +5. ICE servers are loaded from preferences (`getIceServers()`). +6. `WCallCommand.Start(media, aesKey, iceServers, relay)` is dispatched to the WebRTC engine. +7. The call invitation is removed from `callInvitations` and the notification is cancelled. + +### 2.3 Reject Call + +1. User taps "Reject" or the invitation times out. +2. `CallManager.endCall(invitation)` is called. +3. `ChatController.apiRejectCall(rh, contact)` notifies the caller. +4. The invitation is removed from `callInvitations`. + +--- + +## 3. Call State Machine + +``` +Outgoing: WaitCapabilities -> InvitationSent -> OfferSent -> AnswerReceived -> Negotiated -> Connected -> Ended +Incoming: InvitationAccepted -> OfferReceived -> Negotiated -> Connected -> Ended +``` + +| State | Description | +|-------|-------------| +| `WaitCapabilities` | Querying local WebRTC capabilities | +| `InvitationSent` | Caller sent invitation via SMP | +| `InvitationAccepted` | Callee accepted, starting WebRTC | +| `OfferSent` | Caller sent SDP offer | +| `OfferReceived` | Callee received SDP offer | +| `AnswerReceived` | Caller received SDP answer | +| `Negotiated` | ICE negotiation complete | +| `Connected` | Media flowing | +| `Ended` | Call terminated | + +--- + +## 4. Ending a Call + +1. User taps the end-call button, or the remote side ends the call. +2. `CallManager.endCall(call)` is called. +3. `ChatController.apiEndCall(rh, contact)` notifies the remote side via SMP. +4. `ChatModel.showCallView` is set to `false`. +5. `ChatModel.activeCall` is set to `null`. +6. On Android, `CallService` is stopped and the `WebView` is destroyed. +7. On Desktop, `WCallCommand.End` is sent to the browser via WebSocket, and the NanoWSD server is stopped. + +--- + +## 5. Android-Specific: CallActivity and CallService + +### 5.1 CallActivity + +- `CallActivity` is a separate `ComponentActivity` (not `MainActivity`). +- It is launched via `platform.androidStartCallActivity(acceptCall, remoteHostId, chatId)`. +- It hosts `ActiveCallView` with a `WebView` for WebRTC. +- Supports lock-screen display: `setShowWhenLocked(true)` and `setTurnScreenOn(true)`. +- Supports Picture-in-Picture (PiP) mode for video calls. + - On Android 12+, PiP auto-enters when the user navigates away. + - On older versions, PiP is entered via `enterPictureInPictureMode()` on `onUserLeaveHint`. + - PiP layout switches to `LayoutType.RemoteVideo` to show only the remote video feed. +- The activity finishes itself when both `invitation == null` and (`!showCallView || call == null`) and `!switchingCall`. + +### 5.2 CallService + +- `CallService` is a foreground `Service` that keeps the process alive during calls. +- Started via `CallService.startService()` which calls `ContextCompat.startForegroundService`. +- Acquires a partial `WakeLock` to prevent CPU sleep. +- Shows a persistent notification with: + - Contact name and call type (audio/video). + - An "End Call" action button. + - A chronometer showing call duration (from `connectedAt`). +- The notification taps open `CallActivity`. +- Foreground service type includes `MICROPHONE`, `CAMERA` (if video), and `MEDIA_PLAYBACK`. + +--- + +## 6. Desktop-Specific: Browser-Based WebRTC + +### 6.1 NanoWSD Embedded Server + +1. When a call starts, `startServer(onResponse)` creates a `NanoWSD` server on `localhost:50395`. +2. The server serves static WebRTC HTML/JS from bundled resources at `/assets/www/desktop/call.html`. +3. The system browser is opened to `http://localhost:50395/simplex/call/`. + +### 6.2 WebSocket Communication + +1. The browser page connects back via WebSocket to the same `localhost:50395` server. +2. Commands from the app to the browser are serialized as `WVAPICall(corrId, command)` JSON. +3. Responses from the browser arrive as `WVAPIMessage(corrId, resp, command)` JSON. +4. The `WebRTCController` composable manages the command queue: + - Collects commands from `ChatModel.callCommand` (a `SnapshotStateList`). + - Sends them to the browser via the WebSocket connection. + - Processes responses through the same `WCallResponse` handling as Android. +5. On dispose, `WCallCommand.End` is sent, the server is stopped, and connections are cleared. + +--- + +## 7. Common Signaling API + +| API Function | Purpose | +|-------------|---------| +| `apiSendCallInvitation(rh, contact, callType)` | Send call invitation via SMP | +| `apiRejectCall(rh, contact)` | Reject incoming call | +| `apiSendCallOffer(rh, contact, rtcSession, rtcIceCandidates, media, capabilities)` | Send SDP offer | +| `apiSendCallAnswer(rh, contact, rtcSession, rtcIceCandidates)` | Send SDP answer | +| `apiSendCallExtraInfo(rh, contact, rtcIceCandidates)` | Send additional ICE candidates | +| `apiEndCall(rh, contact)` | End active call | +| `apiCallStatus(rh, contact, status)` | Report WebRTC connection status | + +--- + +## 8. In-Call Media Controls + +During an active call, the user can toggle media sources via `WCallCommand.Media(source, enable)`: + +| Source | Control | +|--------|---------| +| `CallMediaSource.Mic` | Mute/unmute microphone | +| `CallMediaSource.Camera` | Enable/disable camera | +| `CallMediaSource.ScreenAudio` | Screen share audio | +| `CallMediaSource.ScreenVideo` | Screen share video | + +Camera switching (front/back) is done via `WCallCommand.Camera(VideoCamera.User / VideoCamera.Environment)`. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `Call` | `views/call/WebRTC.kt` | Active call state: contact, callState, media sources, encryption | +| `CallState` | `views/call/WebRTC.kt` | Enum: WaitCapabilities through Ended | +| `RcvCallInvitation` | `views/call/WebRTC.kt` | Incoming call invitation with contact, callType, sharedKey | +| `CallManager` | `views/call/CallManager.kt` | Manages call lifecycle: accept, end, report | +| `WCallCommand` | `views/call/WebRTC.kt` | Commands to WebRTC engine: Capabilities, Start, Offer, Answer, Ice, Media, Camera, End | +| `WCallResponse` | `views/call/WebRTC.kt` | Responses from WebRTC: Capabilities, Offer, Answer, Ice, Connection, Connected, End | +| `CallActivity` | `android/.../views/call/CallActivity.kt` | Android Activity hosting the call UI and WebView | +| `CallService` | `android/.../CallService.kt` | Android foreground Service for call persistence | +| `NanoWSD` | `desktopMain/.../views/call/CallView.desktop.kt` | Desktop embedded HTTP+WebSocket server | diff --git a/apps/multiplatform/product/flows/connection.md b/apps/multiplatform/product/flows/connection.md new file mode 100644 index 0000000000..1b1123b535 --- /dev/null +++ b/apps/multiplatform/product/flows/connection.md @@ -0,0 +1,233 @@ +# Connection Flow + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/api.md](../../spec/api.md) + +## Overview + +Establishing a contact connection in SimpleX Chat follows an invitation-link model. One party creates a connection link (one-time invitation or long-term address), shares it out-of-band, and the other party connects via that link. The process uses SMP queues for the handshake, with no central server involved in identity management. + +Connections support incognito mode, where a random profile is used per-connection instead of the user's real profile. + +## Prerequisites + +- Chat is initialized and running. +- An active user profile exists. +- For connecting: a valid SimpleX connection link (invitation or address). + +--- + +## 1. Creating a Connection Link (Inviter Side) + +### 1.1 One-Time Invitation Link + +1. User navigates to "New Chat" and selects "Add Contact" (or uses the "+" action). +2. `ChatController.apiAddContact(rh, incognito)` is called: + +```kotlin +suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair?, (() -> Unit)?> +``` + +3. Internally, `CC.APIAddContact(userId, incognito)` is sent to the core. +4. The core creates a new SMP queue pair and returns: + - `CR.Invitation` with `connLinkInvitation: CreatedConnLink` and `connection: PendingContactConnection`. +5. The `CreatedConnLink` contains the invitation URI (long form and short link). +6. The link is displayed as a QR code in `NewChatView` and can be copied or shared. +7. A `PendingContactConnection` appears in the chat list while waiting. + +### 1.2 Long-Term Contact Address + +1. User goes to Settings and creates a SimpleX address. +2. This creates a persistent address link that multiple people can use. +3. Incoming connection requests from the address require explicit acceptance (see section 4). + +--- + +## 2. Connecting via Link (Connector Side) + +### 2.1 Preview the Connection Plan + +Before connecting, the link is analyzed: + +```kotlin +suspend fun apiConnectPlan(rh: Long?, connLink: String, inProgress: MutableState): Pair? +``` + +1. User pastes or scans a link. +2. `apiConnectPlan` sends `CC.APIConnectPlan(userId, connLink)` to the core. +3. The core resolves short links, validates the link, and returns a `ConnectionPlan`: + +```kotlin +sealed class ConnectionPlan { + class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() + class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan() + class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan() + class Error(val chatError: ChatError): ConnectionPlan() +} +``` + +4. For `InvitationLinkPlan`: + - `Ok`: Fresh invitation, safe to connect. + - `OwnLink`: User's own link, alert shown. + - `Connecting(contact_)`: Already connecting to this contact. + - `Known(contact)`: Already connected, existing contact shown. + +5. For `ContactAddressPlan`: + - `Ok`: Fresh address, safe to connect. + - `OwnLink`: User's own address. + - `ConnectingConfirmReconnect`: Was connecting, offer to retry. + - `ConnectingProhibit(contact)`: Connection in progress, cannot duplicate. + - `Known(contact)`: Already a contact. + - `ContactViaAddress(contact)`: Contact already exists via this address. + +6. For `GroupLinkPlan`: + - `Ok`: Fresh group link, safe to join. + - `OwnLink(groupInfo)`: User's own group. + - `ConnectingConfirmReconnect`: Was connecting, offer to retry. + - `ConnectingProhibit(groupInfo_)`: Connection in progress. + - `Known(groupInfo)`: Already a member. + +### 2.2 High-Level Connect Flow: planAndConnect + +The `planAndConnect` function in `ConnectPlan.kt` orchestrates the full connect experience: + +```kotlin +suspend fun planAndConnect( + rhId: Long?, + shortOrFullLink: String, + close: (() -> Unit)?, + cleanup: (() -> Unit)? = null, + filterKnownContact: ((Contact) -> Unit)? = null, + filterKnownGroup: ((GroupInfo) -> Unit)? = null, +): CompletableDeferred +``` + +1. A progress indicator is shown. +2. `apiConnectPlan` is called to analyze the link. +3. Based on the plan type, the appropriate UI is shown: + - For `Ok` plans: proceed to `apiConnect`. + - For `Known`: navigate to the existing contact/group. + - For `OwnLink`: show alert. + - For `Connecting`: show reconnect confirmation or prohibit. +4. Returns a `CompletableDeferred` indicating success. + +### 2.3 Execute Connection + +```kotlin +suspend fun apiConnect(rh: Long?, incognito: Boolean, connLink: CreatedConnLink): PendingContactConnection? +``` + +1. `CC.APIConnect(userId, incognito, connLink)` is sent to the core. +2. The core initiates the SMP handshake: + - For invitation links: `CR.SentConfirmation` is returned. + - For contact addresses: `CR.SentInvitation` is returned. +3. A `PendingContactConnection` is returned and appears in the chat list. +4. The connect progress indicator is shown via `ConnectProgressManager`. + +--- + +## 3. Connection Handshake Completion + +### 3.1 For Invitation Links + +1. After the connector sends confirmation, the inviter's core receives it. +2. Both sides complete the SMP handshake automatically. +3. A `CR.ContactConnected` event is received on both sides. +4. The `PendingContactConnection` in the chat list is replaced by a full `Contact`. +5. Both parties can now exchange messages. + +### 3.2 For Contact Addresses + +1. The connector's confirmation arrives as a `ContactRequest` on the address owner's side. +2. The address owner must explicitly accept or reject (see section 4). +3. Once accepted, the handshake completes and `CR.ContactConnected` fires. + +--- + +## 4. Contact Request Acceptance + +### 4.1 Accept a Contact Request + +```kotlin +suspend fun apiAcceptContactRequest(rh: Long?, incognito: Boolean, contactReqId: Long): Contact? +``` + +1. The address owner sees a contact request notification in the chat list. +2. User taps to open and selects "Accept". +3. `CC.ApiAcceptContact(incognito, contactReqId)` is sent to the core. +4. The core responds with `CR.AcceptingContactRequest` and a `Contact` object. +5. The SMP handshake continues; once complete, `CR.ContactConnected` fires. +6. The `incognito` flag determines whether the real profile or a random profile is shared. + +### 4.2 Reject a Contact Request + +```kotlin +suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Contact? +``` + +1. User selects "Reject" on the contact request. +2. `CC.ApiRejectContact(contactReqId)` is sent to the core. +3. The core responds with `CR.ContactRequestRejected`. +4. The contact request is removed from the chat list. +5. The connector's side eventually times out or receives an error. + +--- + +## 5. Incognito Mode + +### 5.1 Per-Connection Incognito + +1. The `incognito` parameter is available on both `apiAddContact` and `apiConnect`. +2. When `incognito = true`: + - A random display name is generated for this connection. + - The real user profile is not shared with the contact. + - The incognito profile is stored per-connection in the database. +3. The global incognito toggle is in `AppPreferences.incognito`. +4. Incognito status is visible in the chat info view. + +### 5.2 Accept with Incognito + +1. When accepting a contact request with `incognito = true`, a random profile is used. +2. The accepted contact only sees the random profile. +3. The user can have some contacts with real profile and others with incognito profiles. + +--- + +## 6. Connection Progress and UI + +### 6.1 ConnectProgressManager + +```kotlin +object ConnectProgressManager { + fun startConnectProgress(text: String, onCancel: (() -> Unit)? = null) + fun stopConnectProgress() + fun cancelConnectProgress() +} +``` + +1. When a connection is initiated, `startConnectProgress` is called. +2. After a 1-second delay, a progress indicator appears if the operation is still in progress. +3. On completion (success or failure), `stopConnectProgress` is called. +4. The user can cancel via `cancelConnectProgress`. + +### 6.2 Pending Connection States + +While connecting, the chat list shows a `PendingContactConnection` with status: +- Waiting for the other party to scan/use the link. +- Connecting (handshake in progress). +- Connected (transitions to a full Contact chat). + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `CreatedConnLink` | `model/SimpleXAPI.kt` | Connection link with full URI and short link | +| `PendingContactConnection` | `model/ChatModel.kt` | In-progress connection shown in chat list | +| `ConnectionPlan` | `model/SimpleXAPI.kt` | Sealed class: InvitationLink, ContactAddress, GroupLink, Error | +| `InvitationLinkPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, Connecting, Known | +| `ContactAddressPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, ConnectingConfirmReconnect, ConnectingProhibit, Known | +| `GroupLinkPlan` | `model/SimpleXAPI.kt` | Ok, OwnLink, ConnectingConfirmReconnect, ConnectingProhibit, Known | +| `ConnectProgressManager` | `model/ChatModel.kt` | Manages connect progress indicator with timeout | +| `Contact` | `model/ChatModel.kt` | Established contact with profile, connection status | +| `ContactRequest` | `model/ChatModel.kt` | Pending inbound contact request | diff --git a/apps/multiplatform/product/flows/file-transfer.md b/apps/multiplatform/product/flows/file-transfer.md new file mode 100644 index 0000000000..edbb565c07 --- /dev/null +++ b/apps/multiplatform/product/flows/file-transfer.md @@ -0,0 +1,252 @@ +# File Transfer Flow + +> **Related spec:** [spec/services/files.md](../../spec/services/files.md) + +## Overview + +SimpleX Chat transfers files using two protocols based on file size: inline delivery through SMP messages for small files, and XFTP (SimpleX File Transfer Protocol) for larger files. All locally stored files can be AES-encrypted via CryptoFile. The system supports automatic receiving of small media, manual download for larger files, and cancellation at any stage. + +## Prerequisites + +- An active chat connection (direct contact or group). +- Sufficient storage space on the device. +- For XFTP: network connectivity to XFTP relay servers. + +--- + +## 1. File Size Thresholds and Constants + +| Constant | Value | Purpose | +|----------|-------|---------| +| `MAX_IMAGE_SIZE` | 261,120 bytes (255 KB) | Maximum inline image thumbnail size (base64 in message body) | +| `MAX_IMAGE_SIZE_AUTO_RCV` | 522,240 bytes (510 KB) | Auto-receive threshold for images | +| `MAX_VOICE_SIZE_AUTO_RCV` | 522,240 bytes (510 KB) | Auto-receive threshold for voice messages | +| `MAX_VIDEO_SIZE_AUTO_RCV` | 1,047,552 bytes (1023 KB) | Auto-receive threshold for video thumbnails | +| `MAX_FILE_SIZE_SMP` | 8,000,000 bytes (~7.6 MB) | Maximum file size for SMP inline transfer | +| `MAX_FILE_SIZE_XFTP` | 1,073,741,824 bytes (1 GB) | Maximum file size for XFTP transfer | +| `MAX_FILE_SIZE_LOCAL` | `Long.MAX_VALUE` | No limit for local files | + +These constants are defined in `views/helpers/Utils.kt`. + +The core decides the transfer protocol: +- Files within the SMP inline threshold are embedded directly in SMP messages. +- Files exceeding the inline threshold (up to 1 GB) use XFTP with chunked, encrypted upload/download through relay servers. + +--- + +## 2. CryptoFile Encryption + +### 2.1 Overview + +When `privacyEncryptLocalFiles` is enabled (default: `true`), files stored on device are AES-GCM encrypted. The encryption/decryption is performed via JNI calls to the Haskell core. + +### 2.2 Key Types + +```kotlin +// model/ChatModel.kt +@Serializable +data class CryptoFileArgs( + val fileKey: String, // AES-256 key (base64) + val fileNonce: String // GCM nonce (base64) +) + +@Serializable +data class CryptoFile { + val filePath: String + val cryptoArgs: CryptoFileArgs? // null for unencrypted files +} +``` + +### 2.3 Write (Encrypt) + +```kotlin +fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs +``` + +1. `ChatController.getChatCtrl()` obtains the active controller handle. +2. Data is placed in a `DirectByteBuffer`. +3. `chatWriteFile(ctrl, path, buffer)` is called via JNI. +4. The core generates a random AES key and nonce, encrypts the data, writes to `path`. +5. Returns `CryptoFileArgs(fileKey, fileNonce)` needed for decryption. +6. On error, throws an exception with the error message. + +### 2.4 Read (Decrypt) + +```kotlin +fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray +``` + +1. `chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce)` is called via JNI. +2. Returns a two-element array: `[status: Int, data: ByteArray]`. +3. If `status == 0`, the decrypted data is returned. +4. Otherwise, an exception is thrown with the error message. + +### 2.5 File-to-File Encryption + +```kotlin +fun encryptCryptoFile(fromPath: String, toPath: String): CryptoFileArgs +``` + +Encrypts a plaintext file at `fromPath` to an encrypted file at `toPath`. Used when saving user-selected files to the app's encrypted storage. + +### 2.6 File-to-File Decryption + +```kotlin +fun decryptCryptoFile(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) +``` + +Decrypts an encrypted file at `fromPath` to plaintext at `toPath`. Used when exporting/sharing files. + +--- + +## 3. Sending Files + +### 3.1 Attach and Send via ComposeView + +1. User attaches a file via the file picker. +2. File size is validated: `fileSize <= MAX_FILE_SIZE_XFTP` (1 GB). +3. If valid, `ComposeState.preview` is set to `ComposePreview.FilePreview(fileName, uri)`. +4. If too large, an alert is shown with the maximum supported size. +5. On send, the file is copied to the app files directory. +6. If `privacyEncryptLocalFiles` is enabled, the file is encrypted via `encryptCryptoFile`, producing a `CryptoFile` with `cryptoArgs`. +7. A `ComposedMessage` is created with: + - `fileSource`: the `CryptoFile` (path + optional cryptoArgs). + - `msgContent`: `MsgContent.MCFile(text)` for generic files, `MsgContent.MCImage(text, thumbnail)` for images, `MsgContent.MCVideo(text, thumbnail, duration)` for videos, or `MsgContent.MCVoice(text, duration)` for voice. +8. `ChatController.apiSendMessages(...)` dispatches the message. +9. The core determines the transfer protocol and begins the upload. + +### 3.2 Standalone File Upload (XFTP) + +For uploading files outside of a chat message context: + +```kotlin +suspend fun uploadStandaloneFile(user: UserLike, file: CryptoFile, ctrl: ChatCtrl? = null): Pair +``` + +1. `CC.ApiUploadStandaloneFile(userId, file)` is sent to the core. +2. On success, `CR.SndStandaloneFileCreated` returns a `FileTransferMeta`. +3. The meta contains a file description URI that can be shared for download. + +### 3.3 Upload Progress + +1. The core emits `SndFileProgressXFTP` events periodically during upload. +2. `CIFileStatus` on the chat item transitions through: + - `SndStored` (queued) + - `SndTransfer(sndProgress, sndTotal)` (uploading) + - `SndComplete` (upload finished, link sent) +3. The UI updates the progress indicator on the file attachment. + +--- + +## 4. Receiving Files + +### 4.1 Auto-Receive + +When `privacyAcceptImages` is enabled (default: `true`), small media files are auto-received: + +1. On receiving a message with a file attachment, the auto-receive logic checks: + - `MCImage` files with `fileSize <= MAX_IMAGE_SIZE_AUTO_RCV` (510 KB) + - `MCVideo` files with `fileSize <= MAX_VIDEO_SIZE_AUTO_RCV` (1023 KB) + - `MCVoice` files with `fileSize <= MAX_VOICE_SIZE_AUTO_RCV` (510 KB) and not already accepted +2. If criteria are met, `receiveFile` is called automatically. + +### 4.2 Manual Receive + +For files that are not auto-received: + +1. The chat item shows a download button with file size info. +2. File size is validated: `fileSizeValid(file)` checks `file.fileSize <= getMaxFileSize(file.fileProtocol)`. +3. User taps the download button. +4. `ChatController.receiveFile(rhId, user, fileId, userApprovedRelays, auto)` is called: + +```kotlin +suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) +``` + +5. This delegates to `receiveFiles` which handles relay approval: + +```kotlin +suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List, userApprovedRelays: Boolean = false, auto: Boolean = false) +``` + +6. For each file, `CC.ReceiveFile(fileId, userApprovedRelays, encrypted, inline)` is sent to the core. +7. If the file requires unapproved XFTP relays, the user is prompted to approve them. +8. Relay approval errors (`FileError.Auth` with `SMP AUTH` and `PROXY BROKER`) trigger relay approval alerts. +9. Other errors are collected and shown after all files are processed. + +### 4.3 Batch Receive + +Multiple files can be received at once: + +```kotlin +suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List, ...) +``` + +1. Iterates through all `fileIds`. +2. Files needing relay approval are batched and prompted once. +3. After approval, those files are retried with `userApprovedRelays = true`. +4. Errors for individual files are aggregated. + +### 4.4 Download Progress + +1. The core emits `RcvFileProgressXFTP` events during download. +2. `CIFileStatus` transitions through: + - `RcvAccepted` (download initiated) + - `RcvTransfer(rcvProgress, rcvTotal)` (downloading) + - `RcvComplete` (download finished) +3. On completion, if the file is encrypted, it remains encrypted on disk with `cryptoArgs` stored in the database. +4. When the user opens/views the file, `readCryptoFile` or `decryptCryptoFile` is called on demand. + +--- + +## 5. Cancelling a File Transfer + +### 5.1 Cancel via API + +```kotlin +suspend fun cancelFile(rh: Long?, user: User, fileId: Long) +``` + +1. `apiCancelFile(rh, fileId)` sends `CC.CancelFile(fileId)` to the core. +2. The core cancels any in-progress upload or download. +3. On success, the chat item is updated via `chatItemSimpleUpdate`. +4. `cleanupFile(chatItem)` removes any partial local files. + +### 5.2 Cancel via UI + +1. User long-presses a file message and selects "Cancel". +2. `cancelFileAlertDialog(fileId, cancelFile, cancelAction)` shows a confirmation dialog. +3. `CancelAction` provides the appropriate alert text based on direction (sending/receiving). +4. On confirmation, `cancelFile` is called. + +### 5.3 Compose Cancel + +Before sending, user can cancel the file attachment: + +1. User taps the "X" on the file preview in the compose area. +2. `ComposeState.preview` is reset to `ComposePreview.NoPreview`. +3. No API call is needed since the file was not yet sent. + +--- + +## 6. File Cleanup + +1. Files pending deletion are tracked in `ChatModel.filesToDelete`. +2. When a chat item with a file is deleted, the file path is added to `filesToDelete`. +3. The actual file deletion happens asynchronously. +4. Encrypted files require no special cleanup beyond deleting the encrypted file; the key exists only in the database record. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `CryptoFile` | `model/ChatModel.kt` | File reference with path and optional encryption args | +| `CryptoFileArgs` | `model/ChatModel.kt` | AES key + nonce for encrypted files | +| `WriteFileResult` | `model/CryptoFile.kt` | Result of `writeCryptoFile`: success with args or error | +| `CIFile` | `model/ChatModel.kt` | Chat item file metadata: fileId, fileName, fileSize, fileStatus, fileProtocol | +| `CIFileStatus` | `model/ChatModel.kt` | File transfer status: SndStored, SndTransfer, SndComplete, RcvInvitation, RcvAccepted, RcvTransfer, RcvComplete, etc. | +| `FileProtocol` | `model/ChatModel.kt` | Transfer protocol: XFTP, SMP, LOCAL | +| `FileTransferMeta` | `model/ChatModel.kt` | Metadata for standalone XFTP uploads | +| `ComposePreview.FilePreview` | `views/chat/ComposeView.kt` | Compose state for file attachment | diff --git a/apps/multiplatform/product/flows/group-lifecycle.md b/apps/multiplatform/product/flows/group-lifecycle.md new file mode 100644 index 0000000000..60311f7b47 --- /dev/null +++ b/apps/multiplatform/product/flows/group-lifecycle.md @@ -0,0 +1,283 @@ +# Group Lifecycle Flow + +> **Related spec:** [spec/api.md](../../spec/api.md) | [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Overview + +Groups in SimpleX Chat are decentralized: there is no central group server. The group owner's device coordinates membership, and messages are delivered via pairwise SMP connections between members. Groups support roles, invitation links, member admission review, blocking, and profile updates. + +## Prerequisites + +- Chat is initialized and running. +- An active user profile exists. +- For creating a group: no special requirements. +- For joining: a group invitation link or a direct invitation from an existing member. + +--- + +## 1. Creating a Group + +### 1.1 Create Group + +1. User navigates to "New Chat" and selects "Create Group". +2. The `AddGroupView` collects a group profile: display name, full name, optional image, and optional description. +3. `ChatController.apiNewGroup(rh, incognito, groupProfile)` is called: + +```kotlin +suspend fun apiNewGroup(rh: Long?, incognito: Boolean, groupProfile: GroupProfile): GroupInfo? +``` + +4. `CC.ApiNewGroup(userId, incognito, groupProfile)` is sent to the core. +5. The core creates the group and returns `CR.GroupCreated` with a `GroupInfo` object. +6. The creating user is automatically assigned the `Owner` role. +7. The new group appears in the chat list. +8. If `incognito = true`, a random profile is used for the user within this group. + +### 1.2 Update Group Profile + +```kotlin +suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile): GroupInfo? +``` + +1. Owner or Admin navigates to group info and edits the profile. +2. `CC.ApiUpdateGroupProfile(groupId, groupProfile)` is sent to the core. +3. On success, `CR.GroupUpdated` returns the updated `GroupInfo` with `toGroup`. +4. The chat model is updated via `chatModel.chatsContext.updateGroup(rh, groupInfo)`. +5. Profile changes are propagated to all connected members. + +--- + +## 2. Adding Members + +### 2.1 Invite a Contact + +1. Owner or Admin opens group info and taps "Add Members". +2. `AddGroupMembersView` displays the user's contacts eligible for invitation. +3. A role is selected for the invitee (default: `Member`). +4. `ChatController.apiAddMember(rh, groupId, contactId, memberRole)` is called: + +```kotlin +suspend fun apiAddMember(rh: Long?, groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? +``` + +5. `CC.ApiAddMember(groupId, contactId, memberRole)` is sent to the core. +6. The core sends a group invitation to the contact via their direct SMP connection. +7. `CR.SentGroupInvitation` returns a `GroupMember` in `Invited` status. +8. The member list updates to show the pending invitation. + +### 2.2 Invitee Joins + +1. The invited contact receives a group invitation event. +2. A group invitation chat item appears in their chat list. +3. The invitee taps "Join" to accept. +4. `ChatController.apiJoinGroup(rh, groupId)` is called. +5. `CC.ApiJoinGroup(groupId)` is sent to the core. +6. `CR.UserAcceptedGroupSent` confirms the join request was sent. +7. The owner's/admin's device processes the join and establishes pairwise connections with existing members. +8. `CR.MemberConnected` events fire as connections to each member are established. + +--- + +## 3. Member Roles + +### 3.1 Role Hierarchy + +```kotlin +enum class GroupMemberRole(val memberRole: String) { + Observer("observer"), // Can only read messages + Author("author"), // Can send messages but limited + Member("member"), // Standard member + Moderator("moderator"), // Can moderate content + Admin("admin"), // Can manage members + Owner("owner") // Full control, can delete group +} +``` + +Selectable roles for assignment: `Observer`, `Member`, `Moderator`, `Admin`, `Owner`. + +### 3.2 Change Member Role + +```kotlin +suspend fun apiMembersRole(rh: Long?, groupId: Long, memberIds: List, memberRole: GroupMemberRole): List +``` + +1. Owner or Admin navigates to member info in `GroupMemberInfoView`. +2. Selects a new role from the role picker. +3. `CC.ApiMembersRole(groupId, memberIds, memberRole)` is sent to the core. +4. The core responds with `CR.MembersRoleUser` returning updated `GroupMember` objects. +5. The change is propagated to all group members. +6. Supports batch role changes (multiple `memberIds`). + +--- + +## 4. Removing and Blocking Members + +### 4.1 Remove Members + +```kotlin +suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean): Pair>? +``` + +1. Owner or Admin selects a member and taps "Remove". +2. `CC.ApiRemoveMembers(groupId, memberIds, withMessages)` is sent. +3. If `withMessages = true`, the removed member's messages are also deleted from all members. +4. `CR.UserDeletedMembers` returns the updated `GroupInfo` and removed `GroupMember` list. +5. The removed member receives a notification and loses access to the group. + +### 4.2 Block Members for All + +```kotlin +suspend fun apiBlockMembersForAll(rh: Long?, groupId: Long, memberIds: List, blocked: Boolean): List +``` + +1. Owner, Admin, or Moderator selects a member and taps "Block for all". +2. `CC.ApiBlockMembersForAll(groupId, memberIds, blocked)` is sent. +3. `blocked = true` blocks; `blocked = false` unblocks. +4. `CR.MembersBlockedForAllUser` returns the updated member list. +5. Blocked members' messages are hidden from all group members. +6. The blocked member can still see the group but their messages are not delivered. + +--- + +## 5. Group Links + +### 5.1 Create Group Link + +```kotlin +suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? +``` + +1. Owner or Admin navigates to group info and taps "Create Group Link". +2. `CC.APICreateGroupLink(groupId, memberRole)` is sent. +3. A default role for joiners is specified (default: `Member`). +4. `CR.GroupLinkCreated` returns a `GroupLink` containing the link URI. +5. The link is displayed in `GroupLinkView` as a QR code and copyable text. + +### 5.2 Update Group Link Role + +```kotlin +suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? +``` + +1. Owner or Admin changes the default role for new members joining via link. +2. `CC.APIGroupLinkMemberRole(groupId, memberRole)` is sent. +3. `CR.CRGroupLink` returns the updated link with the new default role. + +### 5.3 Get Group Link + +```kotlin +suspend fun apiGetGroupLink(rh: Long?, groupId: Long): GroupLink? +``` + +1. Retrieves the existing group link for display. +2. `CC.APIGetGroupLink(groupId)` is sent. +3. Returns `null` if no link exists. + +### 5.4 Delete Group Link + +```kotlin +suspend fun apiDeleteGroupLink(rh: Long?, groupId: Long): Boolean +``` + +1. Owner or Admin navigates to group link settings and taps "Delete Link". +2. `CC.APIDeleteGroupLink(groupId)` is sent. +3. `CR.GroupLinkDeleted` confirms deletion. +4. The link becomes invalid; anyone with the old link can no longer join. + +--- + +## 6. Member Admission Workflow + +### 6.1 Admission Configuration + +Group owners can require review of new members before they are fully admitted: + +```kotlin +data class GroupMemberAdmission( + val review: MemberCriteria? = null +) + +enum class MemberCriteria { + All // All joining members require review +} +``` + +1. Owner opens group info and navigates to "Member Admission" (`MemberAdmissionView`). +2. The `review` field is set to `MemberCriteria.All` to require review of all new members. +3. The admission configuration is saved by updating the group profile: + - `groupProfile.copy(memberAdmission = admission)` is passed to `apiUpdateGroup`. +4. Changes are tracked with unsaved-changes detection (save/discard prompt on navigation). + +### 6.2 Accept a Pending Member + +```kotlin +suspend fun apiAcceptMember(rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole): Pair? +``` + +1. When admission review is enabled, new members joining via link arrive in a pending state. +2. Owner or Admin sees pending members in the member support chat / member list. +3. User selects "Accept" and optionally adjusts the role. +4. `CC.ApiAcceptMember(groupId, groupMemberId, memberRole)` is sent. +5. `CR.MemberAccepted` returns the updated `GroupInfo` and accepted `GroupMember`. +6. The member is now fully connected and can participate in the group. + +### 6.3 Reject a Pending Member + +1. Owner or Admin selects "Reject" on a pending member. +2. The member is removed via `apiRemoveMembers`. +3. The rejected member receives a removal notification. + +--- + +## 7. Leaving a Group + +```kotlin +suspend fun apiLeaveGroup(rh: Long?, groupId: Long): GroupInfo? +``` + +1. User navigates to group info and taps "Leave Group". +2. A confirmation dialog is shown. +3. `CC.ApiLeaveGroup(groupId)` is sent to the core. +4. `CR.LeftMemberUser` returns the updated `GroupInfo`. +5. The user's membership status changes and they can no longer send or receive messages. +6. The group remains in the chat list in a "left" state, and can be deleted locally. + +--- + +## 8. Listing Members + +```kotlin +suspend fun apiListMembers(rh: Long?, groupId: Long): List +``` + +1. When opening group info or the member list, `apiListMembers` is called. +2. `CC.ApiListMembers(groupId)` is sent to the core. +3. `CR.GroupMembers` returns the member list. +4. `ChatModel.groupMembers` and `ChatModel.groupMembersIndexes` are updated. +5. `ChatModel.membersLoaded` is set to `true`. + +--- + +## 9. Group Chat Scope (Support Channels) + +Groups support scoped conversations for member support: + +- `GroupChatScope` parameter on message APIs allows sending messages within a specific scope (e.g., member support chat). +- `MemberSupportChatView` and `MemberSupportView` provide UI for admin-to-member private conversations within the group context. +- `GroupReportsView` shows moderation reports scoped to the group. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `GroupInfo` | `model/ChatModel.kt` | Group metadata: groupId, groupProfile, membership, fullGroupPreferences | +| `GroupProfile` | `model/ChatModel.kt` | Group display info: displayName, fullName, description, image, memberAdmission | +| `GroupMember` | `model/ChatModel.kt` | Member info: groupMemberId, memberRole, memberStatus, memberProfile | +| `GroupMemberRole` | `model/ChatModel.kt` | Enum: Observer, Author, Member, Moderator, Admin, Owner | +| `GroupMemberAdmission` | `model/ChatModel.kt` | Admission settings: review criteria | +| `MemberCriteria` | `model/ChatModel.kt` | Enum: All (require review for all) | +| `GroupLink` | `model/SimpleXAPI.kt` | Group link: connLinkContact, acceptMemberRole, userContactLinkId, shortLinkDataSet, shortLinkLargeDataSet, groupLinkId | +| `GroupChatScope` | `model/ChatModel.kt` | Scoped conversation within a group | +| `ConnectionPlan.GroupLink` | `model/SimpleXAPI.kt` | Plan result when connecting via a group link | diff --git a/apps/multiplatform/product/flows/messaging.md b/apps/multiplatform/product/flows/messaging.md new file mode 100644 index 0000000000..771eae1c4e --- /dev/null +++ b/apps/multiplatform/product/flows/messaging.md @@ -0,0 +1,195 @@ +# Messaging Flow + +> **Related spec:** [spec/client/compose.md](../../spec/client/compose.md) | [spec/api.md](../../spec/api.md) + +## Overview + +Messaging is the core interaction in SimpleX Chat. Users compose and send text, images, video, voice notes, files, and link previews. Messages can be replied to, edited, deleted, forwarded, and reacted to with emoji. Special modes include timed (disappearing) messages, live messages (real-time typing), and message reports for moderation. + +All message operations flow through the Haskell core via `ChatController.apiSendMessages`, with responses updating `ChatModel` and triggering Compose UI recomposition. + +## Prerequisites + +- Chat is initialized and running (`ChatModel.chatRunning == true`). +- An active user exists (`ChatModel.currentUser != null`). +- A chat is open (`ChatModel.chatId != null`) with an established connection. + +--- + +## 1. Sending a Text Message + +### 1.1 Compose and Send + +1. User types in the compose field. `ComposeState.message` is updated as a `ComposeMessage(text, selection)`. +2. The compose area tracks context via `ComposeContextItem`: `NoContextItem` for a fresh message, `QuotedItem` for a reply, `EditingItem` for an edit, `ForwardingItems` for forwarding, or `ReportedItem` for a report. +3. User taps the send button. The `ComposeView` builds a `ComposedMessage`: + ```kotlin + class ComposedMessage( + val fileSource: CryptoFile?, + val quotedItemId: Long?, + val msgContent: MsgContent, + val mentions: Map + ) + ``` +4. For plain text, `msgContent` is `MsgContent.MCText(text)`. +5. `ChatController.apiSendMessages(rh, type, id, scope, live, ttl, composedMessages)` is called. +6. The core command `CC.ApiSendMessages` is dispatched via `sendCmd`. +7. On success, the response `CR.NewChatItems` returns a list of `AChatItem`. +8. `ChatModel` is updated and the chat item list recomposes to show the new message. +9. `ComposeState` is reset to its default. + +### 1.2 Link Preview + +1. As the user types, the text is parsed for URLs. +2. If `privacyLinkPreviews` preference is enabled and a URL is detected, a `LinkPreview` is fetched asynchronously. +3. The compose preview is set to `ComposePreview.CLinkPreview(linkPreview)`. +4. When sent, the `msgContent` is `MsgContent.MCLink(text, preview)`. + +--- + +## 2. Sending Media (Image, Video, Voice) + +### 2.1 Image + +1. User picks or captures an image. +2. The image is resized (max inline data size `MAX_IMAGE_SIZE` = 255 KB for the preview thumbnail). +3. The full-size file is saved to the app files directory. +4. If local file encryption is enabled (`privacyEncryptLocalFiles`), the file is encrypted via `encryptCryptoFile`, producing a `CryptoFile` with `CryptoFileArgs(fileKey, fileNonce)`. +5. Compose preview becomes `ComposePreview.MediaPreview(images, content)`. +6. On send, `msgContent` is `MsgContent.MCImage(text, imageBase64)` and `fileSource` is the `CryptoFile`. +7. The core handles inline delivery (for small files) or XFTP upload (for larger files). + +### 2.2 Video + +1. User picks or records a video. +2. A thumbnail image is extracted and resized. +3. The video file is saved and optionally encrypted. +4. On send, `msgContent` is `MsgContent.MCVideo(text, image, duration)`. + +### 2.3 Voice Message + +1. User records a voice note. Recording state is tracked via `RecordingState` (NotStarted, Started, Finished). +2. The compose preview becomes `ComposePreview.VoicePreview(voice, durationMs, finished)`. +3. On send, `msgContent` is `MsgContent.MCVoice(text, durationSeconds)`. +4. A file attachment carries the actual audio data. + +--- + +## 3. Sending Files + +1. User picks a file via the file chooser. +2. File size is validated against `MAX_FILE_SIZE_XFTP` (1 GB). +3. Compose preview becomes `ComposePreview.FilePreview(fileName, uri)`. +4. On send, `msgContent` is `MsgContent.MCFile(text)` and the `fileSource` is populated. +5. Delivery via inline (small files under SMP threshold) or XFTP (large files) is determined by the core. + +--- + +## 4. Receiving Messages + +1. The `ChatController` receiver loop calls `chatRecvMsgWait` on the Haskell core. +2. Incoming messages arrive as `CR.NewChatItems` events. +3. `ChatModel` chat items list is updated, triggering recomposition. +4. For media messages, images below `MAX_IMAGE_SIZE_AUTO_RCV` (510 KB), videos below `MAX_VIDEO_SIZE_AUTO_RCV` (1023 KB), and voice notes below `MAX_VOICE_SIZE_AUTO_RCV` (510 KB) are auto-received if `privacyAcceptImages` is enabled. +5. Larger files require manual download initiation (see File Transfer Flow). + +--- + +## 5. Editing a Message + +1. User long-presses a sent message and selects "Edit". +2. `ComposeContextItem` becomes `EditingItem(chatItem)`. +3. The original text populates the compose field. +4. On send, `ChatController.apiUpdateChatItem(rh, type, id, scope, itemId, updatedMessage, live)` is called. +5. `updatedMessage` is an `UpdatedMessage(msgContent, mentions)`. +6. The core responds with `CR.ChatItemUpdated` or `CR.ChatItemNotChanged`. +7. The chat item in `ChatModel` is updated in place. + +--- + +## 6. Deleting a Message + +1. User long-presses a message and selects "Delete". +2. A delete mode is chosen: `CIDeleteMode.cidmBroadcast` (delete for everyone), `CIDeleteMode.cidmInternal` (delete for self), or `CIDeleteMode.cidmInternalMark` (mark as deleted internally). +3. `ChatController.apiDeleteChatItems(rh, type, id, scope, itemIds, mode)` is called. +4. The core responds with `CR.ChatItemsDeleted`, returning a list of `ChatItemDeletion`. +5. For group chats by moderators, `apiDeleteMemberChatItems(rh, groupId, itemIds)` is used. +6. Deleted items are either removed from the UI or replaced with a "deleted" marker. + +--- + +## 7. Reacting to a Message + +1. User long-presses a message and selects an emoji reaction. +2. `ChatController.apiChatItemReaction(rh, type, id, scope, itemId, add, reaction)` is called. +3. `reaction` is a `MsgReaction` (typically emoji). +4. `add = true` to add, `add = false` to remove a reaction. +5. The core responds with `CR.ChatItemReaction`, and the chat item's reaction list is updated. +6. In groups, `apiGetReactionMembers` can be called to see who reacted. + +--- + +## 8. Replying to a Message + +1. User swipes or long-presses a message and selects "Reply". +2. `ComposeContextItem` becomes `QuotedItem(chatItem)`. +3. The quoted item preview is shown above the compose field. +4. On send, the `ComposedMessage.quotedItemId` is set to the quoted item's ID. +5. The sent message renders with the quoted content inline. + +--- + +## 9. Forwarding Messages + +1. User selects one or more messages and taps "Forward". +2. `ChatController.apiPlanForwardChatItems(rh, fromChatType, fromChatId, fromScope, chatItemIds)` is called first to get a `CR.ForwardPlan` with forwardable/non-forwardable item categorization. +3. `ComposeContextItem` becomes `ForwardingItems(chatItems, fromChatInfo)`. +4. User picks a destination chat. +5. `ChatController.apiForwardChatItems(rh, toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl)` is called. +6. New chat items are created in the destination chat. + +--- + +## 10. Timed (Disappearing) Messages + +1. Timed messages are enabled per-chat via chat feature preferences. +2. When composing, a TTL (time-to-live) in seconds is passed as the `ttl` parameter to `apiSendMessages`. +3. The core attaches the TTL to the message metadata. +4. After the TTL expires, the message is automatically deleted on both sides. +5. The UI shows a countdown indicator on timed messages via `CIMetaView`. + +--- + +## 11. Live Messages + +1. User enables live message mode (long-press on send button if `liveMessageAlertShown` preference allows). +2. `ComposeState.liveMessage` is set to a `LiveMessage(chatItem, typedMsg, sentMsg, sent)`. +3. As the user types, `apiSendMessages` is called with `live = true` for the initial send, then `apiUpdateChatItem` with `live = true` for subsequent updates. +4. The recipient sees the message content updating in real-time. +5. When the user finalizes (taps send), a final `apiUpdateChatItem` with `live = false` is sent. + +--- + +## 12. Message Reports + +1. User long-presses a message and selects "Report". +2. `ComposeContextItem` becomes `ReportedItem(chatItem, reason)` where `reason` is a `ReportReason`. +3. On send, `msgContent` is `MsgContent.MCReport(text, reason)`. +4. The report is sent to group owners/admins for moderation review. +5. Group admins see reports in the `GroupReportsView`. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `ComposeState` | `views/chat/ComposeView.kt` | Tracks compose field state | +| `ComposePreview` | `views/chat/ComposeView.kt` | Preview type: NoPreview, CLinkPreview, MediaPreview, VoicePreview, FilePreview | +| `ComposeContextItem` | `views/chat/ComposeView.kt` | Context: NoContextItem, QuotedItem, EditingItem, ForwardingItems, ReportedItem | +| `ComposedMessage` | `model/SimpleXAPI.kt` | Wire format for sending: fileSource, quotedItemId, msgContent, mentions | +| `UpdatedMessage` | `model/SimpleXAPI.kt` | Wire format for editing: msgContent, mentions | +| `MsgContent` | `model/ChatModel.kt` | Sealed class: MCText, MCLink, MCImage, MCVideo, MCVoice, MCFile, MCReport, MCChat, MCUnknown | +| `LiveMessage` | `views/chat/ComposeView.kt` | Tracks live message state | +| `MsgReaction` | `model/ChatModel.kt` | Emoji reaction type | +| `ChatItemDeletion` | `model/ChatModel.kt` | Deletion result with old/new item | diff --git a/apps/multiplatform/product/flows/onboarding.md b/apps/multiplatform/product/flows/onboarding.md new file mode 100644 index 0000000000..b6b3e835a5 --- /dev/null +++ b/apps/multiplatform/product/flows/onboarding.md @@ -0,0 +1,205 @@ +# Onboarding Flow + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/architecture.md](../../spec/architecture.md) + +## Overview + +Onboarding is the first-run experience that initializes the Haskell chat core, creates the local database, sets up the user profile, configures server operators, and (on Android) selects the notification mode. The flow is tracked by the `OnboardingStage` enum persisted in `AppPreferences.onboardingStage`. + +The initialization path differs slightly between Android and Desktop, but both converge on the common `chatMigrateInit` JNI call and shared `ChatController` logic. + +## Prerequisites + +- Fresh install or database reset. +- On Android: `SimplexApp.onCreate()` has been called. +- On Desktop: `main()` has been called. + +--- + +## 1. Platform Initialization + +### 1.1 Android: SimplexApp.onCreate() + +1. `SimplexApp.onCreate()` is called by the Android framework. +2. `AppContextProvider.initialize(this)` sets the application context. +3. Phoenix process detection: if this is a restart process, return early. +4. A global error handler is registered. +5. `initHaskell(packageName)` loads the native `libapp-lib.so` and calls `initHS()` to initialize the Haskell runtime. +6. `initMultiplatform()` sets up: + - `androidAppContext` reference. + - `ntfManager` (notification manager bridge to Android `NtfManager`). + - `platform` interface implementation with Android-specific callbacks for services, notifications, call management, and UI configuration. +7. `reconfigureBroadcastReceivers()` ensures notification-related receivers match saved preferences. +8. `runMigrations()` performs any pending app-level data migrations. +9. Temp directory is cleaned and recreated. +10. If a migration state exists (`chatModel.migrationState.value != null`), onboarding is forced to `Step1_SimpleXInfo`. +11. Otherwise, if authentication keys are available, `initChatControllerOnStart()` is called. + +### 1.2 Desktop: Main.kt main() + +1. `initHaskell()` loads native libraries: + - On Linux/macOS: `libapp-lib.so` / `libapp-lib.dylib`. + - On Windows: `libcrypto-3-x64.dll`, `libsimplex.dll`, `libapp-lib.dll` plus VLC libraries. +2. `initHS()` initializes the Haskell runtime. +3. `platform` interface is set with Desktop-specific callbacks (app update notice). +4. `runMigrations()` performs pending app-level data migrations. +5. `setupUpdateChecker()` configures the desktop update channel. +6. `initApp()` initializes common app state. +7. Temp directory is cleaned and recreated. +8. `showApp()` launches the Compose Desktop window, which renders the `AppView`. + +--- + +## 2. Database Initialization (chatMigrateInit) + +### 2.1 initChatController + +1. `initChatController(useKey, confirmMigrations, startChat)` is called (from `Core.kt`). +2. If `ctrlInitInProgress` is already true, return (prevents double initialization). +3. The database key is resolved: + - From `useKey` parameter if provided. + - Otherwise from `DatabaseUtils.useDatabaseKey()` which reads from the keystore. +4. Migration confirmation mode is determined: + - `MigrationConfirmation.YesUp` (auto-confirm forward migrations) by default. + - `MigrationConfirmation.Error` if developer tools + confirm upgrades are enabled. +5. `chatMigrateInit(dbPath, dbKey, confirm)` is called via JNI. This: + - Opens (or creates) the SQLite database at `dbAbsolutePrefixPath`. + - Runs all pending schema migrations. + - Returns a `ChatCtrl` handle (Long) and a `DBMigrationResult`. +6. On `DBMigrationResult.OK`: + - The `ChatCtrl` is stored globally. + - `ChatModel.chatDbStatus` is set. + - App file paths are configured via `apiSetAppFilePaths`. + - `apiGetActiveUser` checks for an existing user. +7. If an active user exists, `startChat(user)` is called. +8. If no user exists, `startChatWithoutUser()` is called and onboarding begins at `Step1_SimpleXInfo`. + +### 2.2 Error Handling + +- `DBMigrationResult.ErrorNotADatabase`: Wrong passphrase or corrupted DB. User is prompted. +- `DBMigrationResult.ErrorMigration`: Migration failed. Details shown to user. +- `DBMigrationResult.ErrorKeyNotSet`: Encryption key missing. +- `DBMigrationResult.InvalidConfirmation`: Migrations need manual confirmation (developer mode). +- On any error, `ChatModel.chatDbStatus` is set and the UI shows the appropriate database error screen. + +--- + +## 3. Onboarding Stages + +The onboarding flow is controlled by `OnboardingStage`, persisted in `AppPreferences.onboardingStage`: + +```kotlin +enum class OnboardingStage { + Step1_SimpleXInfo, + Step2_CreateProfile, + LinkAMobile, + Step2_5_SetupDatabasePassphrase, + Step3_ChooseServerOperators, + Step3_CreateSimpleXAddress, + Step4_SetNotificationsMode, + OnboardingComplete +} +``` + +### 3.1 Step1_SimpleXInfo + +1. The `SimpleXInfo` screen is shown. +2. Explains what SimpleX Chat is: privacy, no user identifiers, decentralized. +3. User taps "Create your profile" to proceed. +4. On Desktop, a "Link a Mobile" option is also available. + +### 3.2 Step2_CreateProfile + +1. The `CreateProfile` screen is shown. +2. User enters a display name (validated via `chatValidName` JNI) and optional full name. +3. On submit, `ChatController.apiCreateActiveUser(rh, profile)` is called: + ```kotlin + suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? + ``` +4. The core command `CC.CreateActiveUser(p, pastTimestamp)` creates the user in the database. +5. On success, `CR.ActiveUser` returns the new `User` object. +6. `ChatModel.currentUser` is set. +7. If the chat is not yet running, `startChat(user)` is called: + - `apiSetNetworkConfig` configures network settings. + - `apiStartChat` starts the message receiver. + - `startReceiver()` begins polling for incoming messages. +8. Onboarding advances to `Step3_ChooseServerOperators`. + +### 3.3 LinkAMobile (Desktop Only) + +1. Available as an alternative to creating a profile on Desktop. +2. Shows a QR code for linking with a mobile device. +3. The desktop acts as a remote host controlled by the mobile app. + +### 3.4 Step2_5_SetupDatabasePassphrase (Desktop Only) + +1. On Desktop, after profile creation, the user is optionally prompted to set a database passphrase. +2. If skipped, a random passphrase is used (`desktopOnboardingRandomPassword` flag). +3. `ChatModel.desktopOnboardingRandomPassword` tracks this state. + +### 3.5 Step3_ChooseServerOperators + +1. The `ChooseServerOperators` screen is shown. +2. User selects which preset server operators to use for messaging and file transfer. +3. Server operator conditions may need to be accepted. +4. The selection is saved via the server configuration APIs. + +### 3.6 Step3_CreateSimpleXAddress + +1. User is prompted to create a SimpleX address for receiving contact requests. +2. This calls the address creation API. +3. Can be skipped. + +### 3.7 Step4_SetNotificationsMode (Android Only) + +1. The `SetNotificationsMode` screen is shown. +2. Three modes are available: + - `NotificationsMode.SERVICE`: Persistent background service (instant notifications). + - `NotificationsMode.PERIODIC`: Periodic background work (delayed notifications). + - `NotificationsMode.OFF`: No background processing (manual check only). +3. On selection, `appPrefs.notificationsMode` is set. +4. On Desktop, this step is skipped entirely. + +### 3.8 OnboardingComplete + +1. `appPrefs.onboardingStage` is set to `OnboardingComplete`. +2. The chat list view (`ChatListView`) is shown. +3. On Android, `SimplexService.showBackgroundServiceNoticeIfNeeded()` may show additional setup prompts. +4. On Android with `NotificationsMode.SERVICE`, `SimplexService.start()` is called. + +--- + +## 4. startChat Flow + +After the user is created and onboarding progresses, `ChatController.startChat(user)` orchestrates the final setup: + +1. `apiSetNetworkConfig(getNetCfg())` applies network configuration. +2. `apiCheckChatRunning()` checks if the core is already running. +3. `listUsers(null)` loads all user profiles into `ChatModel.users`. +4. If chat is not running: + - `ChatModel.currentUser` is set. + - `apiStartChat()` starts the core's message processing. + - `startReceiver()` begins the message receive loop. + - `setLocalDeviceName` sets the device name for remote access. +5. `apiGetChats` loads the chat list. +6. `chatModel.chatsContext.updateChats(chats)` populates the UI. +7. User address and chat item TTL are loaded. +8. `appPrefs.chatLastStart` is updated. +9. `ChatModel.chatRunning` is set to `true`. +10. `platform.androidChatInitializedAndStarted()` is called for Android-specific post-start tasks. + +--- + +## Key Types Reference + +| Type | Location | Purpose | +|------|----------|---------| +| `OnboardingStage` | `views/onboarding/OnboardingView.kt` | Enum tracking onboarding progress | +| `SimplexApp` | `android/.../SimplexApp.kt` | Android Application class, entry point | +| `Main.kt` | `desktop/.../Main.kt` | Desktop entry point | +| `ChatController` | `model/SimpleXAPI.kt` | Core API controller, manages chat lifecycle | +| `ChatModel` | `model/ChatModel.kt` | Global observable state | +| `DBMigrationResult` | `views/helpers/DatabaseUtils.kt` | Database migration outcome | +| `chatMigrateInit` | `platform/Core.kt` | JNI function: initialize DB and run migrations | +| `initChatController` | `platform/Core.kt` | High-level initialization orchestrator | +| `AppPreferences` | `model/SimpleXAPI.kt` | Persistent user preferences | diff --git a/apps/multiplatform/product/gaps.md b/apps/multiplatform/product/gaps.md new file mode 100644 index 0000000000..25535d8003 --- /dev/null +++ b/apps/multiplatform/product/gaps.md @@ -0,0 +1,290 @@ +# Known Gaps & Recommendations -- SimpleX Chat (Android & Desktop, Kotlin Multiplatform) + +This document catalogs known gaps in the multiplatform codebase (Android and Desktop) with severity, impact, and recommendations. + +--- + +## Table of Contents + +1. [UI: Error Feedback](#gap-01-ui-error-feedback) +2. [UI: Loading States](#gap-02-ui-loading-states) +3. [Security: Database Passphrase Not Enforced](#gap-03-security-database-passphrase-not-enforced) +4. [Security: No Forward Secrecy Indicator](#gap-04-security-no-forward-secrecy-indicator) +5. [Documentation: Haskell Store Layer Not Fully Specified](#gap-05-documentation-haskell-store-layer-not-fully-specified) +6. [Desktop: Recording Not Implemented](#gap-06-desktop-recording-not-implemented) +7. [Desktop: Cryptor Not Implemented](#gap-07-desktop-cryptor-not-implemented) + +--- + +## GAP-01: UI Error Feedback + +**Severity:** Medium +**Category:** UI / UX +**Platforms:** Android, Desktop + +### Description + +Many API calls through `ChatController.sendCmd()` return `API.Error` responses that are logged but not surfaced to the user. The general pattern is: + +```kotlin +val r = sendCmd(rh, cmd) +if (r is API.Result && r.res is CR.ExpectedResponse) return r.res.value +Log.e(TAG, "someFunction bad response: ${r.responseType} ${r.details}") +return null +``` + +When the call fails, the caller receives `null` and either silently does nothing or shows a generic error. The specific `ChatError` details (which may contain actionable information like quota exceeded, server unreachable, or store errors) are lost to the user. + +### Affected Locations + +- `SimpleXAPI.kt` -- `getAgentSubsTotal()`, `getAgentServersSummary()`, and dozens of similar `api*` functions +- Throughout the codebase wherever `sendCmd` results are pattern-matched + +### Impact + +Users experience silent failures with no indication of what went wrong. This is particularly problematic for: +- Connection attempts that fail due to network issues +- File transfer failures +- Group operations that fail due to role permissions +- Server configuration errors + +### Recommendation + +1. Introduce a structured error-handling utility that maps `ChatError` subtypes to user-visible messages, similar to how `retryableNetworkErrorAlert` already handles a subset of `AgentErrorType.BROKER` errors. +2. At minimum, surface a dismissible snackbar/toast with a summary when an API call fails unexpectedly. +3. For critical operations (send message, join group, create connection), show a dialog with retry/cancel options (the `sendCmdWithRetry` pattern already exists for some cases -- extend it). + +--- + +## GAP-02: UI Loading States + +**Severity:** Low-Medium +**Category:** UI / UX +**Platforms:** Android, Desktop + +### Description + +Several long-running operations lack loading indicators, leaving the user uncertain whether the action is in progress. The `ComposeState.inProgress` flag and `progressByTimeout` mechanism exist for the compose area, and `ConnectProgressManager` handles connection progress, but many other flows have no visual feedback. + +### Affected Locations + +- Group member list loading (`ChatModel.membersLoaded` exists but is not always checked before displaying stale data) +- Server configuration validation (`ApiValidateServers` can take several seconds with no indicator) +- Database export/import (`ApiExportArchive`, `ApiImportArchive`) +- Profile switching (`changeActiveUser_` acquires `changingActiveUserMutex` but the UI may appear frozen) + +### Impact + +Users may tap actions multiple times, causing duplicate requests, or assume the app is frozen and force-quit during a long operation like database export. + +### Recommendation + +1. Introduce a centralized `ProgressOverlay` composable that can be shown/hidden via a `ChatModel` flag. +2. Wrap all operations that acquire `changingActiveUserMutex` or take > 1 second with a visible loading state. +3. Use `ChatModel.switchingUsersAndHosts` (which already exists) more consistently as a gate for showing a blocking progress indicator. + +--- + +## GAP-03: Security: Database Passphrase Not Enforced + +**Severity:** High +**Category:** Security +**Platforms:** Android, Desktop + +### Description + +When the app is first installed, a random database passphrase is generated and stored in encrypted preferences. The user is never required to set a custom passphrase. The `initialRandomDBPassphrase` flag tracks this state, and a setup prompt exists in onboarding (`SetupDatabasePassphrase`), but the user can skip it. + +On Android, the encrypted passphrase is stored via the Android Keystore, which provides hardware-backed security. On Desktop, the `Cryptor` is a **placeholder** (see GAP-07), meaning the passphrase is stored in plaintext. + +### Affected Locations + +- `SimpleXAPI.kt` -- `AppPreferences.storeDBPassphrase`, `AppPreferences.initialRandomDBPassphrase`, `AppPreferences.encryptedDBPassphrase` +- `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` +- `common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt` + +### Impact + +- Users who skip passphrase setup rely entirely on device security. If the device is compromised, the database can be decrypted using the stored passphrase. +- On Desktop, the passphrase is effectively stored in plaintext (see GAP-07), meaning anyone with filesystem access can read the database. + +### Recommendation + +1. Consider making passphrase setup mandatory during onboarding (or at least prominently warn users who skip it). +2. On Desktop, implement proper key storage (GAP-07) before any passphrase enforcement is meaningful. +3. Add a periodic reminder for users who still have `initialRandomDBPassphrase == true`. + +--- + +## GAP-04: Security: No Forward Secrecy Indicator + +**Severity:** Medium +**Category:** Security / UI +**Platforms:** Android, Desktop + +### Description + +The double-ratchet algorithm provides forward secrecy per message, and PQ key exchange provides resistance to quantum attacks. The `Connection` type tracks `pqSupport`, `pqEncryption`, `pqSndEnabled`, and `pqRcvEnabled`. However, the UI does not prominently display the current forward secrecy state or PQ encryption status for a given conversation. + +### Affected Locations + +- `ChatModel.kt` -- `Connection.pqSupport`, `Connection.pqEncryption`, `Connection.pqSndEnabled`, `Connection.pqRcvEnabled` +- Contact info views, group member info views + +### Impact + +Users cannot easily verify whether their conversations are using PQ-enhanced encryption. Security-conscious users have no visual indicator of the ratchet state or whether PQ key exchange was successful. + +### Recommendation + +1. Add a security badge/icon in the chat header or contact info screen showing: + - Whether PQ key exchange is active (both peers support it) + - Whether the connection has been verified (security code comparison) + - The ratchet state (in-sync vs. needs re-sync) +2. The `connectionCode` field on `Connection` can be used to show verification status. +3. The `Call.encryptionStatus` pattern (used in call views) could be adapted for the chat view. + +--- + +## GAP-05: Documentation: Haskell Store Layer Not Fully Specified + +**Severity:** Medium +**Category:** Documentation / Architecture +**Platforms:** Android, Desktop + +### Description + +The Kotlin client communicates with the Haskell core via a text-based command protocol (`CC.cmdString` -> FFI -> Haskell). The Haskell store layer (SQLite operations, migration logic, and the exact semantics of `StoreError` variants) is not documented from the Kotlin side. The `ChatErrorStore` error type wraps a `StoreError` whose variants are defined in Haskell and deserialized by the Kotlin client, but the conditions under which each error occurs are not specified. + +### Affected Locations + +- `SimpleXAPI.kt:6986` -- `ChatErrorStore(storeError: StoreError)` +- `SimpleXAPI.kt` -- `StoreError` sealed class (deserialized from Haskell responses) +- `SimpleXAPI.kt` -- `ChatErrorDatabase(databaseError: DatabaseError)` for migration errors + +### Impact + +- Developers cannot predict which `StoreError` will occur for a given operation without reading the Haskell source. +- Error handling in the Kotlin layer is necessarily generic since the error semantics are not specified. +- Migration failures (`ChatErrorDatabase`) are particularly opaque. + +### Recommendation + +1. Create a specification document mapping each `CC` command to its possible `StoreError` / `DatabaseError` responses. +2. Document the database migration versioning scheme and the conditions under which `confirmDBUpgrades` is triggered. +3. Add inline documentation to the `StoreError` sealed class variants explaining their trigger conditions. + +--- + +## GAP-06: Desktop: Recording Not Implemented + +**Severity:** High +**Category:** Feature / Platform +**Platform:** Desktop only + +### Description + +The `RecorderNative` class on Desktop is a placeholder. Both `start()` and `stop()` are stubbed with `/*LALAL*/` comments and return dummy values (empty string and 0, respectively). Users cannot record voice messages on Desktop. + +```kotlin +// common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +actual class RecorderNative: RecorderInterface { + override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String { + /*LALAL*/ + return "" + } + + override fun stop(): Int { + /*LALAL*/ + return 0 + } +} +``` + +Audio playback IS implemented on Desktop (via VLC/`vlcj` library), so received voice messages can be played. Only recording is missing. + +### Affected Locations + +- `common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt:15-25` +- `common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt` -- `RecorderInterface` + +### Impact + +Desktop users cannot send voice messages. The record button either does nothing or produces a zero-length file. + +### Recommendation + +1. Implement `RecorderNative` using a JVM audio capture library (e.g., `javax.sound.sampled`, or integrate with the existing `vlcj` dependency for capture). +2. The output format should match the mobile app's voice message format (likely Opus in an OGG container) for cross-platform compatibility. +3. Until implemented, the record button should be hidden or disabled on Desktop with a tooltip explaining the limitation. + +### Additional Desktop LALAL Placeholders + +Several other Desktop features are also marked with `LALAL` placeholders: +- **QR Code Scanner** (`QRCodeScanner.desktop.kt:12`) -- scanning QR codes is not implemented on Desktop +- **Animated Drawables** (`Utils.desktop.kt:179`) -- animated image support (e.g., GIF in-line rendering) is not implemented +- **Animated Chat Images** (`CIImageView.desktop.kt:19`) -- animated image rendering in chat items +- **isImage detection** (`Images.desktop.kt:168`) -- image type detection (implemented but marked as incomplete) + +--- + +## GAP-07: Desktop: Cryptor Not Implemented + +**Severity:** Critical +**Category:** Security / Platform +**Platform:** Desktop only + +### Description + +The `CryptorInterface` implementation on Desktop is a non-functional placeholder. All three methods are stubbed: + +```kotlin +// common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt +actual val cryptor: CryptorInterface = object : CryptorInterface { + override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? { + return String(data) // LALAL + } + + override fun encryptText(text: String, alias: String): Pair { + return text.toByteArray() to text.toByteArray() // LALAL + } + + override fun deleteKey(alias: String) { + // LALAL + } +} +``` + +- `decryptData` returns the data as-is (no decryption) +- `encryptText` returns the plaintext as both "encrypted data" and "IV" +- `deleteKey` is a no-op + +### Affected Locations + +- `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` +- `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` -- `CryptorInterface` +- `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` -- uses `cryptor` for passphrase encryption + +### Impact + +**This is a critical security gap.** On Desktop: +- The database passphrase is stored **in plaintext** in the preferences file. Anyone with read access to the user's home directory can extract the passphrase and decrypt the database. +- The self-destruct passphrase is similarly stored in plaintext. +- The app passphrase (for local authentication) provides no real protection. +- Key deletion is a no-op, so "deleting" a key has no effect. + +This directly undermines RULE-02 (Database Encryption at Rest) and RULE-04 (Self-Destruct Profile) on the Desktop platform. + +### Recommendation + +1. **Priority: Critical.** Implement proper key storage on Desktop using one of: + - **OS Keychain integration:** macOS Keychain, Windows Credential Manager, Linux Secret Service (via `libsecret`/GNOME Keyring/KWallet) + - **Java Cryptography Architecture (JCA)** with a PKCS#12 keystore file protected by a master password + - **Bouncy Castle** library for platform-independent key management +2. Until a real implementation exists, display a prominent warning to Desktop users that their database passphrase is not securely stored. +3. Consider requiring the user to enter their passphrase on each app launch (do not store it) as an interim measure. + +### Related + +- GAP-03 (Database Passphrase Not Enforced) is compounded by this gap on Desktop. +- The `testCrypto()` function referenced in `AppCommon.desktop.kt:39` is commented out with a `// LALAL` marker, suggesting crypto testing was planned but never completed. diff --git a/apps/multiplatform/product/glossary.md b/apps/multiplatform/product/glossary.md new file mode 100644 index 0000000000..10203d8a2a --- /dev/null +++ b/apps/multiplatform/product/glossary.md @@ -0,0 +1,561 @@ +# Domain Term Glossary -- SimpleX Chat (Android & Desktop, Kotlin Multiplatform) + +This glossary is self-contained and covers the Android and Desktop (Kotlin/Compose Multiplatform) codebase only. + +--- + +## Table of Contents + +1. [Protocols & Cryptography](#1-protocols--cryptography) +2. [Core Data Types](#2-core-data-types) +3. [Commands & Events](#3-commands--events) +4. [Connection & Identity](#4-connection--identity) +5. [Messaging Features](#5-messaging-features) +6. [Calling & Media](#6-calling--media) +7. [Notifications & Background](#7-notifications--background) +8. [Application Architecture](#8-application-architecture) +9. [Configuration & Preferences](#9-configuration--preferences) + +--- + +## 1. Protocols & Cryptography + +### SMP (SimpleX Messaging Protocol) +The core message-relay protocol. Clients send and receive messages through SMP relay servers without exposing sender/receiver identity correlation. The protocol uses unidirectional queues -- each contact pair maintains separate send and receive queues on potentially different servers. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `SMPErrorType`, `SMPProxyMode`, `SMPProxyFallback`, `SMPWebPortServers` + +### XFTP (SimpleX File Transfer Protocol) +Protocol for transferring files through relay servers. Files are chunked, encrypted, and uploaded to XFTP relays. Recipients download chunks and reassemble locally. Supports inline transfer for small files. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `CC.ApiUploadStandaloneFile`, `CC.ApiDownloadStandaloneFile`, `CC.ApiStandaloneFileInfo` + +### E2E Encryption (End-to-End Encryption) +All messages are encrypted end-to-end. The app never transmits plaintext to relay servers. Encryption keys are negotiated during connection establishment using X3DH-like key agreement and then maintained via the double-ratchet algorithm. + +### Double Ratchet +The core key-management algorithm. After initial key agreement, each message derives a new symmetric key, providing forward secrecy per message. Ratchet state can be re-synchronized via `APISyncContactRatchet` / `APISyncGroupMemberRatchet` commands. + +*See:* `SimpleXAPI.kt` -- `CC.APISyncContactRatchet(contactId, force)`, `CC.APISyncGroupMemberRatchet(groupId, groupMemberId, force)`, `CR.ContactRatchetSync`, `CR.GroupMemberRatchetSync` + +### PQ (Post-Quantum) +Post-quantum key exchange support. Connections track PQ state via `Connection.pqSupport`, `Connection.pqEncryption`, `Connection.pqSndEnabled`, and `Connection.pqRcvEnabled` fields. When both peers support PQ, the key exchange incorporates a post-quantum KEM to resist future quantum attacks. + +*See:* `ChatModel.kt` -- `Connection.pqSupport`, `Connection.pqEncryption`; `SimpleXAPI.kt` -- `SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED` (legacy, no longer used) + +### SMP Proxy / Private Routing +Messages can be sent through an intermediate SMP proxy relay to hide the sender's IP from the destination relay. Controlled by `SMPProxyMode` (Always, Unknown, Unprotected, Never) and `SMPProxyFallback` (Allow, AllowProtected, Prohibit). + +*See:* `SimpleXAPI.kt` -- `AppPreferences.networkSMPProxyMode`, `AppPreferences.networkSMPProxyFallback` + +### Transport Session Mode +Controls how TCP sessions to SMP relays are multiplexed. Options: `User` (one session per user profile), `Session` (single shared session), `Server` (one per server), `Entity` (one per queue/entity -- maximum metadata protection). + +*See:* `SimpleXAPI.kt` -- `AppPreferences.networkSessionMode`, `TransportSessionMode` + +--- + +## 2. Core Data Types + +### ChatItem +A single item in a conversation -- a sent or received message, call event, group event, connection event, feature change, or moderation action. Contains direction (`CIDirection`), metadata (`CIMeta`), content (`CIContent`), optional formatted text, mentions, quoted item, reactions, and file attachment. + +*See:* `ChatModel.kt:2720` -- `data class ChatItem` + +### ChatInfo +The top-level discriminated union representing a conversation. Variants: +- `ChatInfo.Direct` -- wraps a `Contact` +- `ChatInfo.Group` -- wraps a `GroupInfo` +- `ChatInfo.Local` -- wraps a `NoteFolder` (saved messages / notes to self) +- `ChatInfo.ContactRequest` -- wraps a `UserContactRequest` +- `ChatInfo.ContactConnection` -- wraps a `PendingContactConnection` +- `ChatInfo.InvalidJSON` -- fallback for unrecognized data + +*See:* `ChatModel.kt:1391` -- `sealed class ChatInfo` + +### CIContent (Chat Item Content) +The content payload of a `ChatItem`. Over 30 variants including: +- `SndMsgContent` / `RcvMsgContent` -- regular message with `MsgContent` +- `SndCall` / `RcvCall` -- call event with status and duration +- `RcvIntegrityError` -- message integrity violation +- `RcvDecryptionError` -- decryption failure with error type and count +- `RcvGroupInvitation` / `SndGroupInvitation` -- group invite +- `RcvGroupEventContent` / `SndGroupEventContent` -- group lifecycle events +- `RcvChatFeature` / `SndChatFeature` -- per-chat feature toggle notifications +- `SndModerated` / `RcvModerated` / `RcvBlocked` -- moderation events +- `RcvDirectEventContent` -- direct chat lifecycle events + +*See:* `ChatModel.kt:3554` -- `sealed class CIContent` + +### MsgContent +The wire-format message body. Variants: `MCText`, `MCLink`, `MCImage`, `MCVideo`, `MCVoice`, `MCFile`, `MCReport`, `MCUnknown`. Each carries text plus optional media/file metadata. + +*See:* `ChatModel.kt` -- `sealed class MsgContent` + +### User +The local user profile. Fields: `userId`, `userContactId`, `localDisplayName`, `profile` (LocalProfile), `fullPreferences` (FullChatPreferences), `activeUser`, `activeOrder`, `showNtfs`, `sendRcptsContacts`, `sendRcptsSmallGroups`, `viewPwdHash` (for hidden profiles), `uiThemes`, `remoteHostId` (Long?), `autoAcceptMemberContacts` (Boolean). + +*See:* `ChatModel.kt:1208` -- `data class User` + +### Contact +A remote contact. Fields: `contactId`, `localDisplayName`, `profile` (LocalProfile), `activeConn` (Connection?), `viaGroup`, `contactUsed`, `contactStatus`, `chatSettings`, `userPreferences`, `mergedPreferences`, `preparedContact`, `contactRequestId`, `contactGroupMemberId`, `chatTags`, `chatItemTTL`. + +*See:* `ChatModel.kt:1711` -- `data class Contact` + +### GroupInfo +Metadata for a group conversation. Fields: `groupId`, `localDisplayName`, `groupProfile` (GroupProfile), `businessChat` (BusinessChatInfo?), `fullGroupPreferences`, `membership` (GroupMember -- the local user's membership), `chatSettings`, `preparedGroup`, `membersRequireAttention`, `chatTags`, `chatItemTTL`. + +*See:* `ChatModel.kt:2004` -- `data class GroupInfo` + +### GroupMember +A member of a group. Fields: `groupMemberId`, `groupId`, `memberId`, `memberRole` (GroupMemberRole), `memberCategory` (GroupMemberCategory), `memberStatus` (GroupMemberStatus), `memberSettings` (GroupMemberSettings), `blockedByAdmin`, `invitedBy`, `localDisplayName`, `memberProfile`, `memberContactId`, `memberContactProfileId`, `activeConn` (Connection?), `supportChat` (GroupSupportChat?). + +*See:* `ChatModel.kt:2177` -- `data class GroupMember` + +### GroupMemberRole +Enumeration of group roles, ordered for comparison: `Observer` < `Author` < `Member` < `Moderator` < `Admin` < `Owner`. Selectable roles for assignment: Observer, Member, Moderator, Admin, Owner. + +*See:* `ChatModel.kt:2369` -- `enum class GroupMemberRole` + +### Connection +An active or pending cryptographic connection to a peer. Fields: `connId`, `agentConnId`, `peerChatVRange` (VersionRange), `connStatus` (ConnStatus), `connLevel`, `viaGroupLink`, `customUserProfileId`, `connectionCode` (SecurityCode?), `pqSupport`, `pqEncryption`, `pqSndEnabled`, `pqRcvEnabled`, `connectionStats`, `authErrCounter`, `quotaErrCounter`. + +*See:* `ChatModel.kt:1882` -- `data class Connection` + +### Chat +A composite type holding `chatInfo` (ChatInfo), `chatItems` (list of ChatItem), and `chatStats` (ChatStats -- unread count, min unread item ID, etc.). Represents a full conversation for the chat list. + +*See:* `ChatModel.kt` -- `data class Chat` + +### PendingContactConnection +Represents an in-progress connection that has not yet been established. Contains the connection link and state but no contact profile yet. + +*See:* `ChatModel.kt` -- referenced in `ChatInfo.ContactConnection` + +### CryptoFile +A file reference that optionally carries `CryptoFileArgs` (key + nonce) for local encryption. `CryptoFile.plain(path)` creates an unencrypted reference. + +*See:* `ChatModel.kt` -- `data class CryptoFile` + +--- + +## 3. Commands & Events + +The codebase uses short type names for the command/event protocol: `CC` (Chat Command), `CR` (Chat Response -- also carries asynchronous events), `API` (top-level response wrapper), and `ChatError` (error hierarchy). There is no separate "ChatEvent" class; asynchronous events from the core (new messages, connection changes, call signaling) are all `CR` subclasses received via the `recvMsg` loop. + +### CC (Chat Command) +The sealed class representing all commands the app can send to the Haskell core library. Over 140 command variants organized by domain: + +**User management:** `ShowActiveUser`, `CreateActiveUser`, `ListUsers`, `ApiSetActiveUser`, `ApiHideUser`, `ApiUnhideUser`, `ApiMuteUser`, `ApiUnmuteUser`, `ApiDeleteUser` + +**Chat lifecycle:** `StartChat`, `CheckChatRunning`, `ApiStopChat`, `ApiSetAppFilePaths`, `ApiSetEncryptLocalFiles` + +**Database:** `ApiExportArchive`, `ApiImportArchive`, `ApiDeleteStorage`, `ApiStorageEncryption`, `TestStorageEncryption` + +**Messaging:** `ApiSendMessages`, `ApiUpdateChatItem`, `ApiDeleteChatItem`, `ApiDeleteMemberChatItem`, `ApiChatItemReaction`, `ApiForwardChatItems`, `ApiPlanForwardChatItems`, `ApiReportMessage` + +**Groups:** `ApiNewGroup`, `ApiAddMember`, `ApiJoinGroup`, `ApiAcceptMember`, `ApiMembersRole`, `ApiBlockMembersForAll`, `ApiRemoveMembers`, `ApiLeaveGroup`, `ApiListMembers`, `ApiUpdateGroupProfile`, `APICreateGroupLink`, `APIDeleteGroupLink`, `APIGetGroupLink`, `ApiAddGroupShortLink` + +**Connections:** `APIAddContact`, `APIConnect`, `APIConnectPlan`, `APIPrepareContact`, `APIPrepareGroup`, `APIConnectPreparedContact`, `APIConnectPreparedGroup`, `ApiConnectContactViaAddress` + +**Contacts:** `ApiDeleteChat`, `ApiClearChat`, `ApiListContacts`, `ApiUpdateProfile`, `ApiSetContactPrefs`, `ApiSetContactAlias` + +**Address:** `ApiCreateMyAddress`, `ApiDeleteMyAddress`, `ApiShowMyAddress`, `ApiAddMyAddressShortLink`, `ApiSetProfileAddress`, `ApiSetAddressSettings` + +**Calls:** `ApiGetCallInvitations`, `ApiSendCallInvitation`, `ApiRejectCall`, `ApiSendCallOffer`, `ApiSendCallAnswer`, `ApiSendCallExtraInfo`, `ApiEndCall`, `ApiCallStatus` + +**Server config:** `ApiGetServerOperators`, `ApiSetServerOperators`, `ApiGetUserServers`, `ApiSetUserServers`, `ApiValidateServers`, `APITestProtoServer` + +**Network:** `APISetNetworkConfig`, `APIGetNetworkConfig`, `APISetNetworkInfo`, `ReconnectServer`, `ReconnectAllServers` + +**Files:** `ReceiveFile`, `CancelFile`, `ApiUploadStandaloneFile`, `ApiDownloadStandaloneFile`, `ApiStandaloneFileInfo` + +**Remote access:** `SetLocalDeviceName`, `ListRemoteHosts`, `StartRemoteHost`, `SwitchRemoteHost`, `StopRemoteHost`, `DeleteRemoteHost`, `StoreRemoteFile`, `GetRemoteFile`, `ConnectRemoteCtrl`, `FindKnownRemoteCtrl`, `ConfirmRemoteCtrl`, `VerifyRemoteCtrlSession`, `ListRemoteCtrls`, `StopRemoteCtrl`, `DeleteRemoteCtrl` + +**Read status:** `ApiChatRead`, `ApiChatItemsRead`, `ApiChatUnread` + +**Settings:** `APISetChatSettings`, `ApiSetMemberSettings`, `APISetChatItemTTL`, `APIGetChatItemTTL`, `APISetChatTTL`, `ApiSaveSettings`, `ApiGetSettings` + +**Ratchet & verification:** `APISwitchContact`, `APISwitchGroupMember`, `APIAbortSwitchContact`, `APIAbortSwitchGroupMember`, `APISyncContactRatchet`, `APISyncGroupMemberRatchet`, `APIGetContactCode`, `APIGetGroupMemberCode`, `APIVerifyContact`, `APIVerifyGroupMember` + +Each command variant has a `cmdString` property that serializes it to the text protocol consumed by the Haskell FFI. + +*See:* `SimpleXAPI.kt:3529` -- `sealed class CC` + +### CR (Chat Response) +The sealed class representing all responses / events received from the Haskell core. Over 130 response types. Examples: + +- `ActiveUser`, `UsersList` -- user management results +- `ChatStarted`, `ChatRunning`, `ChatStopped` -- lifecycle +- `ApiChats`, `ApiChat` -- chat list data +- `NewChatItems`, `ChatItemUpdated`, `ChatItemsDeleted` -- message events +- `ContactConnected`, `ContactConnecting`, `ContactSndReady` -- connection lifecycle +- `GroupCreated`, `ReceivedGroupInvitation`, `JoinedGroupMemberConnecting`, `MemberAccepted` -- group events +- `RcvFileStart`, `RcvFileComplete`, `SndFileComplete` -- file transfer progress +- `CallInvitation`, `CallOffer`, `CallAnswer`, `CallExtraInfo`, `CallEnded` -- call signaling +- `ChatError` -- error wrapper + +*See:* `SimpleXAPI.kt:6114` -- `sealed class CR` + +### API +The top-level response wrapper. Two variants: +- `API.Result(remoteHostId, res: CR)` -- successful response +- `API.Error(remoteHostId, err: ChatError)` -- error response + +Properties: `ok` (Boolean -- true if `CR.CmdOk`), `result` (CR?), `rhId` (Long? -- remote host ID). + +*See:* `SimpleXAPI.kt:5975` -- `sealed class API` + +### ChatError +The error hierarchy returned from the Haskell core: +- `ChatErrorChat(errorType: ChatErrorType)` -- application-level errors (NoActiveUser, UserUnknown, DifferentActiveUser, etc.) +- `ChatErrorAgent(agentError: AgentErrorType)` -- SMP agent errors (BROKER, SMP, PROXY, etc.) +- `ChatErrorStore(storeError: StoreError)` -- database/store errors +- `ChatErrorDatabase(databaseError: DatabaseError)` -- database migration/encryption errors +- `ChatErrorRemoteHost(remoteHostError)` -- remote host control errors +- `ChatErrorRemoteCtrl(remoteCtrlError)` -- remote controller errors +- `ChatErrorInvalidJSON(json)` -- parse failure + +*See:* `SimpleXAPI.kt:6974` -- `sealed class ChatError` + +### sendCmd / recvMsg +The core FFI bridge. `sendCmd(rhId, cmd)` serializes a `CC` command and sends it to the Haskell backend via `chatSendCmd`. `recvMsg(ctrl)` blocks on `chatRecvMsg` to receive the next `API` response/event. The receiver loop runs in `ChatController.startReceiver()` on `Dispatchers.IO`. + +*See:* `SimpleXAPI.kt` -- `ChatController.sendCmd()`, `ChatController.startReceiver()` + +--- + +## 4. Connection & Identity + +### SimpleX Address (User Address) +A long-lived contact address that others can use to send connection requests. Created via `ApiCreateMyAddress`, retrieved via `ApiShowMyAddress`, deleted via `ApiDeleteMyAddress`. Can optionally include a short link (`ApiAddMyAddressShortLink`). Stored as `ChatModel.userAddress` (`UserContactLinkRec`). + +### Contact Link / Connection Link +A one-time or reusable invitation link. The `CreatedConnLink` type wraps the link string. Contact links can be one-time (single use) or long-lived (user address). Created via `APIAddContact` (one-time) or `ApiCreateMyAddress` (reusable). + +### Group Link +A reusable invitation link for joining a group. Created via `APICreateGroupLink(groupId, memberRole)`. The default role for new members joining via the link is configurable. Can also have a short link variant via `ApiAddGroupShortLink`. + +### Short Link +A compact form of a contact or group link. Created via `ApiAddMyAddressShortLink` (for user addresses) or `ApiAddGroupShortLink` (for groups). Short links resolve to the full connection link data including `ContactShortLinkData` or `GroupShortLinkData`. + +### Incognito Mode +When enabled (`AppPreferences.incognito`), the app generates a random profile name for new connections instead of using the user's real profile. Each connection gets a unique random identity. The `customUserProfileId` on a `Connection` tracks which incognito profile is used for that connection. + +*See:* `SimpleXAPI.kt` -- `AppPreferences.incognito`; `ChatModel.kt` -- `Connection.customUserProfileId` + +### Hidden Profile +A user profile protected by a password (`viewPwdHash`). Hidden profiles do not appear in the profile list unless unlocked with the password. Created via `ApiHideUser(userId, viewPwd)`, revealed via `ApiUnhideUser(userId, viewPwd)`. When switching away from a hidden profile, its notifications are cancelled. + +*See:* `SimpleXAPI.kt` -- `CC.ApiHideUser`, `CC.ApiUnhideUser`; `ChatModel.kt` -- `User.viewPwdHash` + +### Connection Verification (Security Code) +Each connection has an optional `SecurityCode` (`Connection.connectionCode`). Users can verify connections out-of-band by comparing security codes displayed via `APIGetContactCode` / `APIGetGroupMemberCode` and confirming via `APIVerifyContact` / `APIVerifyGroupMember`. + +### Connection Plan +Before connecting via a link, `APIConnectPlan` analyzes the link and returns a `ConnectionPlan` indicating whether the link leads to an existing contact, a new contact, a group join, etc. This prevents duplicate connections. + +*See:* `SimpleXAPI.kt` -- `CC.APIConnectPlan`, `CR.CRConnectionPlan` + +### Prepared Contact / Prepared Group +An intermediate state in the connection flow. `APIPrepareContact` / `APIPrepareGroup` creates the local record and displays the contact/group preview before the user confirms the connection. The user can then change the active profile (`APIChangePreparedContactUser` / `APIChangePreparedGroupUser`) and finally confirm via `APIConnectPreparedContact` / `APIConnectPreparedGroup`. + +--- + +## 5. Messaging Features + +### Delivery Receipt +Confirmation that a message was delivered to the recipient's device. Controlled per-user via `sendRcptsContacts` and `sendRcptsSmallGroups` on `User`. The global setting flow is triggered by `ChatModel.setDeliveryReceipts`. Individual overrides per-contact are managed via `ApiSetUserContactReceipts` / `ApiSetUserGroupReceipts`. + +*See:* `SimpleXAPI.kt` -- `CC.SetAllContactReceipts`, `CC.ApiSetUserContactReceipts`, `CC.ApiSetUserGroupReceipts`; `AppPreferences.privacyDeliveryReceiptsSet` + +### Timed Message (Disappearing Message) +Messages with a time-to-live after which they are automatically deleted. Configured as a `ChatFeature` / `GroupFeature` with a TTL parameter in seconds. The `customDisappearingMessageTime` preference stores the last custom duration used. Per-chat TTL can be set via `APISetChatTTL`. Global TTL via `APISetChatItemTTL`. + +*See:* `SimpleXAPI.kt` -- `CC.APISetChatItemTTL`, `CC.APISetChatTTL`; `AppPreferences.customDisappearingMessageTime` + +### Live Message +A message that updates in real-time as the sender types. Controlled by `CC.ApiSendMessages` with `live=true`. The `ComposeState.liveMessage` tracks the current live message being composed. An alert is shown on first use (`AppPreferences.liveMessageAlertShown`). + +### Message Reactions +Emoji reactions on messages. Added/removed via `ApiChatItemReaction(type, id, scope, itemId, add, reaction)`. Reaction members in groups can be queried via `ApiGetReactionMembers`. Each `ChatItem` carries a `reactions: List`. + +### Message Forwarding +Messages can be forwarded between chats. `ApiPlanForwardChatItems` checks feasibility (e.g., file availability), and `ApiForwardChatItems` performs the forward. A `ForwardConfirmation` may be required if files need downloading first. + +### Message Reports +Users can report messages in groups via `ApiReportMessage(groupId, chatItemId, reportReason, reportText)`. Admins can archive (`ApiArchiveReceivedReports`) or delete (`ApiDeleteReceivedReports`) reports. + +### Mentions +In-message mentions of group members. Stored as `mentions: Map` on `ChatItem` and `mentions: MentionedMembers` on `ComposeState`. + +### Link Previews +Automatic preview generation for URLs in messages. Controlled by `AppPreferences.privacyLinkPreviews`. An alert is shown on first use (`privacyLinkPreviewsShowAlert`). + +### Local File Encryption +Files stored on device can be encrypted. Controlled by `AppPreferences.privacyEncryptLocalFiles` and toggled via `CC.ApiSetEncryptLocalFiles(enable)`. + +### Chat Tags +User-defined tags for organizing conversations. CRUD via `ApiCreateChatTag`, `ApiUpdateChatTag`, `ApiDeleteChatTag`, `ApiReorderChatTags`. Assignment via `ApiSetChatTags`. The model tracks `userTags`, `presetTags` (system-defined categories), `unreadTags`, and the active filter (`activeChatTagFilter`). + +--- + +## 6. Calling & Media + +### WebRTC +The real-time communication framework used for audio and video calls. The app uses WebRTC for peer-to-peer media streams, with SMP used only for call signaling (offer/answer/ICE candidates). + +### Call (data class) +Represents an active call session. Fields: `remoteHostId`, `userProfile`, `contact`, `callUUID`, `callState` (CallState enum), `initialCallType` (Audio/Video), `localMediaSources`, `localCapabilities`, `peerMediaSources`, `sharedKey` (for E2E call encryption), `connectionInfo`, `connectedAt`. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt:14` + +### CallState +Enum tracking call progression: `WaitCapabilities` -> `InvitationSent` / `InvitationAccepted` -> `OfferSent` / `OfferReceived` -> `Negotiated` -> `Connected` -> `Ended`. + +### WCallCommand / WCallResponse +The command/response protocol between the Kotlin app and the WebRTC JavaScript layer: +- **Commands:** `Capabilities`, `Permission`, `Start`, `Offer`, `Answer`, `Ice`, `Media`, `Camera`, `Description`, `Layout`, `End` +- **Responses:** `Capabilities`, `Offer`, `Answer`, `Ice`, `Connection`, `Connected`, `PeerMedia`, `End`, `Ended`, `Ok`, `Error` + +*See:* `WebRTC.kt:88` -- `sealed class WCallCommand`; `WebRTC.kt:103` -- `sealed class WCallResponse` + +### CallManager +Manages incoming call invitations and the active call lifecycle. Handles reporting new incoming calls, accepting calls, switching between calls, and ending calls. Interacts with `ChatModel.callInvitations`, `ChatModel.activeCall`, and the platform notification manager. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt` + +### Android: CallActivity +A dedicated Android `Activity` that displays the call UI. Launched when accepting an incoming call or initiating an outgoing call. Uses an Android `WebView` to host the WebRTC JavaScript. + +*See:* `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` + +### Android: CallService +An Android foreground `Service` that keeps the call alive when the app is in the background. Holds a `WakeLock`, displays an ongoing call notification, and manages the call lifecycle. Uses notification channel `CALL_SERVICE_NOTIFICATION`. + +*See:* `android/src/main/java/chat/simplex/app/CallService.kt` + +### Desktop: Browser-based WebRTC via NanoWSD +On Desktop, calls are implemented by opening the system browser to a locally-hosted WebSocket server. A `NanoHTTPD`/`NanoWSD` server runs on `localhost:50395`, serving the WebRTC call page and communicating with the Kotlin app via WebSocket messages. Commands are sent as JSON-serialized `WVAPICall` objects; responses are parsed as `WVAPIMessage` objects. + +*See:* `common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt` + +### ICE Servers +STUN/TURN servers used for WebRTC NAT traversal. Configurable via `AppPreferences.webrtcIceServers`. The relay policy (`AppPreferences.webrtcPolicyRelay`) controls whether calls must use TURN relays (for IP privacy) or can attempt direct connections. + +### CallMediaType +Enum: `Video`, `Audio`. Determines the initial media type of the call. + +### CallMediaSource +Enum: `Mic`, `Camera`, `ScreenAudio`, `ScreenVideo`. Used in `WCallCommand.Media` to toggle individual media streams. + +--- + +## 7. Notifications & Background + +### Android: SimplexService +A foreground `Service` that keeps the chat backend running in the background. Uses a `WakeLock` and displays a persistent notification ("SimpleX Chat service" channel). Started with `START_STICKY` for automatic restart. Manages the `chatRecvMsg` loop indirectly by keeping the process alive. + +Notification channel: `chat.simplex.app.SIMPLEX_SERVICE_NOTIFICATION` ("SimpleX Chat service") + +*See:* `android/src/main/java/chat/simplex/app/SimplexService.kt` + +### Android: MessagesFetcherWorker +A `WorkManager` periodic worker that wakes the app to fetch new messages when the foreground service is not running (i.e., when `NotificationsMode` is `PERIODIC`). Provides a battery-friendly alternative to the always-on service. + +*See:* `android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt` + +### Android: NotificationsMode +Enum controlling background message fetching: +- `OFF` -- no background activity; messages received only when app is open +- `PERIODIC` -- uses `MessagesFetcherWorker` for periodic fetches +- `SERVICE` -- uses `SimplexService` foreground service (default) + +*See:* `SimpleXAPI.kt:7739` -- `enum class NotificationsMode` + +### Android: Notification Channels +Android notification channels registered by the app: +- **Messages:** `chat.simplex.app.MESSAGE_NOTIFICATION` -- high importance, for incoming messages +- **Calls:** `chat.simplex.app.CALL_NOTIFICATION_2` -- high importance, for incoming call alerts with custom sound +- **Service:** `chat.simplex.app.SIMPLEX_SERVICE_NOTIFICATION` -- low importance, persistent foreground service indicator +- **Call Service:** `chat.simplex.app.CALL_SERVICE_NOTIFICATION` -- default importance, ongoing call indicator + +*See:* `android/src/main/java/chat/simplex/app/model/NtfManager.android.kt`, `SimplexService.kt`, `CallService.kt` + +### Android: NtfManager +The Android-specific notification manager. Handles creating notification channels, displaying message notifications (with grouping via `MessageGroup`), displaying incoming call notifications (with full-screen intent for lock-screen calls), and managing notification actions (accept/reject call, open chat). + +*See:* `android/src/main/java/chat/simplex/app/model/NtfManager.android.kt` + +### Desktop: System Notifications +On Desktop, notifications use the system notification mechanism (typically via the JVM's `SystemTray` or platform-specific notification APIs). The notification manager interface is shared (`ntfManager`) but the implementation is platform-specific. + +### NotificationPreviewMode +Controls what information appears in notifications: +- `HIDDEN` -- no message content +- `CONTACT` -- shows sender name only +- `MESSAGE` -- shows sender name and message preview (default) + +*See:* `ChatModel.kt:4823` -- `enum class NotificationPreviewMode` + +### Wake Lock Management +In `ChatController.startReceiver()`, each received message acquires a wake lock (via `getWakeLock(timeout=60000)`) that is released after 30 seconds. This ensures the device stays awake long enough to process incoming messages and display notifications, particularly for incoming calls. + +--- + +## 8. Application Architecture + +### ChatController +The singleton controller that bridges the Kotlin UI layer and the Haskell core library. Responsibilities: +- Manages the `chatCtrl` (FFI handle to the Haskell runtime) +- Sends commands via `sendCmd()` and receives events via the `startReceiver()` coroutine loop +- Processes received messages in `processReceivedMsg()` +- Holds a reference to `AppPreferences` and `ChatModel` +- Provides the `messagesChannel` (Kotlin coroutine `Channel`) for consumers to observe events +- Manages retry logic for transient network errors (`sendCmdWithRetry`) + +*See:* `SimpleXAPI.kt:493` -- `object ChatController` + +### ChatModel +The singleton reactive state container for the entire app. Uses Compose `mutableStateOf` and `mutableStateListOf` for reactive UI updates. Key state: +- `currentUser` -- the active user profile +- `users` -- list of all user profiles (`UserInfo`) +- `chatsContext` / `secondaryChatsContext` -- `ChatsContext` holding the chat list +- `chatId` -- currently open chat +- `groupMembers` -- members of the currently viewed group +- `callInvitations` -- pending incoming call invitations +- `activeCall` -- the currently active call +- `userAddress` -- the user's SimpleX address +- `chatItemTTL` -- global message TTL setting +- `userTags` -- chat tags +- `terminalItems` -- debug terminal log items +- Various UI state flags (`showCallView`, `switchingUsersAndHosts`, `clearOverlays`, etc.) + +*See:* `ChatModel.kt:86` -- `object ChatModel` + +### AppPreferences +A class wrapping platform-specific key-value storage (`Settings` from `com.russhwolf.settings`). On Android, backed by `SharedPreferences`. On Desktop, backed by Java `Properties` files. Provides type-safe accessors for all user preferences. + +*See:* `SimpleXAPI.kt:94` -- `class AppPreferences` + +### ComposeState +Data class holding the state of the message composition area. Fields: `message` (ComposeMessage), `parsedMessage` (formatted text), `liveMessage`, `preview` (ComposePreview), `contextItem` (ComposeContextItem -- reply/edit context), `inProgress`, `progressByTimeout`, `useLinkPreviews`, `mentions`. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt:98` + +### ModalManager +Manages the modal/sheet presentation stack. Supports multiple placements (default, center, fullscreen, end). Holds an ordered list of `ModalViewHolder` items and exposes `showModal`, `showCustomModal`, `showModalCloseable`, `closeModal`. Uses Compose state (`modalCount`) to trigger recomposition. + +*See:* `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt:92` + +### AlertManager +Singleton for displaying alert dialogs. Provides `showAlertMsg`, `showAlertDialog`, `showAlertDialogButtons`, etc. Works with `AlertManager.shared` for the default instance. + +### ChatsContext +Holds the chat list state for a particular scope (main or secondary). Manages `chats` (State>), provides `updateChats()` to refresh, and supports filtering/keeping specific chats during updates. + +### ConnectProgressManager +Tracks and displays connection progress in the UI. Methods: `startConnectProgress(text, onCancel)`, `stopConnectProgress()`, `cancelConnectProgress()`. Exposes `showConnectProgress` (nullable string indicating active progress text). + +*See:* `ChatModel.kt:48` -- `object ConnectProgressManager` + +### withBGApi / withLongRunningApi +Utility functions for launching coroutines on background threads. Used throughout the codebase to perform API calls without blocking the UI thread. + +--- + +## 9. Configuration & Preferences + +### AppPreferences (Storage) +All preferences are accessed through `ChatController.appPrefs`, which is a lazy-initialized `AppPreferences` instance. The underlying storage is: +- **Android:** `SharedPreferences` with ID `chat.simplex.app.SIMPLEX_APP_PREFS` +- **Desktop:** Java `Properties` files via `com.russhwolf.settings` + +Theme overrides have separate storage (`SHARED_PREFS_THEMES_ID`). + +### SharedPreference +A generic wrapper providing `get()` and `set(value)` for a single preference. All `AppPreferences` fields are `SharedPreference` instances created by factory methods (`mkBoolPreference`, `mkStrPreference`, `mkIntPreference`, `mkLongPreference`, `mkFloatPreference`, `mkEnumPreference`, `mkSafeEnumPreference`, `mkDatePreference`, `mkMapPreference`, `mkTimeoutPreference`). + +### Key Preference Categories + +**Notifications:** +- `notificationsMode` -- OFF / PERIODIC / SERVICE +- `notificationPreviewMode` -- HIDDEN / CONTACT / MESSAGE +- `canAskToEnableNotifications` -- gate for the notification prompt + +**Privacy:** +- `privacyProtectScreen` -- prevents screenshots (Android FLAG_SECURE) +- `privacyAcceptImages` -- auto-accept inline images +- `privacyLinkPreviews` -- generate URL previews +- `privacySanitizeLinks` -- strip tracking parameters from URLs +- `privacyShowChatPreviews` -- show message preview in chat list +- `privacySaveLastDraft` -- persist draft messages +- `privacyEncryptLocalFiles` -- encrypt files at rest +- `privacyAskToApproveRelays` -- prompt before using relays suggested by contacts +- `privacyMediaBlurRadius` -- blur radius for media in notifications/previews + +**Security:** +- `performLA` -- require local authentication (biometric/PIN) +- `laMode` -- local authentication mode +- `laLockDelay` -- seconds before re-locking +- `storeDBPassphrase` -- whether to persist the DB passphrase +- `initialRandomDBPassphrase` -- indicates the DB uses a random (non-user-chosen) passphrase +- `selfDestruct` -- enable self-destruct profile +- `selfDestructDisplayName` -- display name for the self-destruct profile + +**Network:** +- `networkUseSocksProxy` -- route traffic through SOCKS proxy +- `networkProxy` -- SOCKS proxy host/port configuration +- `networkSessionMode` -- transport session multiplexing mode +- `networkSMPProxyMode` -- SMP proxy / private routing mode +- `networkSMPProxyFallback` -- fallback behavior when proxy fails +- `networkHostMode` -- onion/public host preference +- `networkRequiredHostMode` -- enforce host mode strictly +- Various TCP timeout settings (background, interactive, per-KB) +- Keep-alive settings (idle, interval, count) + +**Calls:** +- `webrtcPolicyRelay` -- force TURN relay usage +- `callOnLockScreen` -- DISABLE / SHOW / ACCEPT calls on lock screen +- `webrtcIceServers` -- custom ICE server configuration +- `experimentalCalls` -- enable experimental call features + +**Appearance:** +- `currentTheme` -- active theme name +- `systemDarkTheme` -- theme for system dark mode +- `themeOverrides` -- per-theme customizations +- `profileImageCornerRadius` -- avatar rounding +- `chatItemRoundness` -- message bubble rounding +- `chatItemTail` -- show/hide message bubble tail +- `fontScale` -- text size scaling +- `densityScale` -- UI density scaling +- `inAppBarsAlpha` -- toolbar transparency +- `appearanceBarsBlurRadius` -- toolbar blur effect + +**UI:** +- `oneHandUI` -- one-handed UI mode (bottom-aligned navigation) +- `chatBottomBar` -- show bottom bar in chat view +- `simplexLinkMode` -- how SimpleX links are displayed (DESCRIPTION / FULL / BROWSER) +- `showUnreadAndFavorites` -- filter chat list to unread/favorites +- `developerTools` -- enable developer tools (terminal, etc.) + +**Database:** +- `encryptedDBPassphrase` -- encrypted form of the DB passphrase +- `initializationVectorDBPassphrase` -- IV for DB passphrase encryption +- `encryptionStartedAt` -- timestamp of encryption operation start (for crash recovery) +- `confirmDBUpgrades` -- prompt before database migrations +- `newDatabaseInitialized` -- flag for incomplete initialization recovery + +**Remote Access:** +- `deviceNameForRemoteAccess` -- device display name for remote control +- `confirmRemoteSessions` -- require confirmation for remote sessions +- `connectRemoteViaMulticast` -- use multicast discovery +- `connectRemoteViaMulticastAuto` -- auto-connect via multicast +- `desktopWindowState` -- persisted window position/size (Desktop only) + +**Migration:** +- `migrationToStage` / `migrationFromStage` -- track migration progress +- `onboardingStage` -- current onboarding step +- `lastMigratedVersionCode` -- last app version that ran migrations + +*See:* `SimpleXAPI.kt:94-489` -- `class AppPreferences` with all `SHARED_PREFS_*` constants diff --git a/apps/multiplatform/product/rules.md b/apps/multiplatform/product/rules.md new file mode 100644 index 0000000000..90a2dadada --- /dev/null +++ b/apps/multiplatform/product/rules.md @@ -0,0 +1,253 @@ +# Business Rules -- SimpleX Chat (Android & Desktop, Kotlin Multiplatform) + +This document specifies invariants enforced by the Android and Desktop (Kotlin/Compose Multiplatform) clients. + +--- + +## Table of Contents + +1. [Security (RULE-01 through RULE-05)](#1-security) +2. [Message Integrity (RULE-06 through RULE-09)](#2-message-integrity) +3. [Group Integrity (RULE-10 through RULE-13)](#3-group-integrity) +4. [File Transfer (RULE-14 through RULE-15)](#4-file-transfer) +5. [Notification Delivery (RULE-16 through RULE-17)](#5-notification-delivery) +6. [Call Integrity (RULE-18)](#6-call-integrity) + +--- + +## 1. Security + +### RULE-01: End-to-End Encryption is Mandatory + +**Invariant:** Every message, file chunk, and call signaling payload MUST be encrypted end-to-end before transmission. The app MUST NOT transmit plaintext content to any relay server. + +**Enforcement:** The Haskell core library handles all encryption. The Kotlin layer never constructs raw SMP messages. All communication flows through `ChatController.sendCmd()` which delegates to the FFI, ensuring the encryption layer cannot be bypassed. + +**Location:** `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` -- `ChatController.sendCmd()`, `chatSendCmd()` FFI call + +--- + +### RULE-02: Database Encryption at Rest + +**Invariant:** The local SQLite database MUST be encrypted. A passphrase (either user-chosen or randomly generated) MUST be set before the database is operational. + +**Enforcement:** On first launch, a random passphrase is generated and stored encrypted via the platform keystore (`CryptorInterface.encryptText`). The `initialRandomDBPassphrase` preference tracks whether the user has set a custom passphrase. Database encryption state is tracked in `ChatModel.chatDbEncrypted`. Encryption/re-encryption is performed via `CC.ApiStorageEncryption(config: DBEncryptionConfig)`. + +**Caveat:** The user is not forced to set a custom passphrase -- the random passphrase is stored in app-accessible encrypted preferences. See GAP: "Database passphrase not enforced." + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` +- `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` -- `CryptorInterface` +- Android: `common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt` -- Android Keystore +- Desktop: `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` -- **placeholder, not implemented** + +--- + +### RULE-03: Local Authentication Gating + +**Invariant:** When local authentication is enabled (`AppPreferences.performLA == true`), the app MUST require biometric/PIN authentication before displaying any chat content. The lock engages after `laLockDelay` seconds of inactivity. + +**Enforcement:** `AppLock.setPerformLA` controls the lock state. The lock delay is configurable via `AppPreferences.laLockDelay` (default 30 seconds). Authentication mode is set via `AppPreferences.laMode` (system biometric or passcode). + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt` +- `SimpleXAPI.kt` -- `AppPreferences.performLA`, `AppPreferences.laMode`, `AppPreferences.laLockDelay` + +--- + +### RULE-04: Self-Destruct Profile + +**Invariant:** When self-destruct is enabled (`AppPreferences.selfDestruct == true`), entering the self-destruct passphrase instead of the real passphrase MUST wipe the database and present a clean profile with `selfDestructDisplayName`. + +**Enforcement:** The self-destruct passphrase is stored separately (`encryptedSelfDestructPassphrase` / `initializationVectorSelfDestructPassphrase`). On Android, `SimplexService` checks for self-destruct on initialization. The comparison happens during the local authentication flow. + +**Location:** +- `SimpleXAPI.kt` -- `AppPreferences.selfDestruct`, `AppPreferences.selfDestructDisplayName` +- `android/src/main/java/chat/simplex/app/SimplexService.kt` -- initialization check + +--- + +### RULE-05: Screen Protection + +**Invariant:** When `AppPreferences.privacyProtectScreen == true` (default), the app MUST prevent screenshots and screen recording. On Android this uses `FLAG_SECURE`; on Desktop this is advisory only. + +**Enforcement:** The preference defaults to `true`. The Android activity applies `FLAG_SECURE` to its window based on this preference. The Desktop app cannot enforce this at the OS level. + +**Location:** `SimpleXAPI.kt` -- `AppPreferences.privacyProtectScreen` + +--- + +## 2. Message Integrity + +### RULE-06: Message Ordering Verification + +**Invariant:** The app MUST detect and surface message integrity violations (gaps, duplicates, out-of-order delivery) to the user. + +**Enforcement:** The Haskell core tracks message sequence numbers per connection. When a gap or integrity error is detected, a `CIContent.RcvIntegrityError(msgError: MsgErrorType)` chat item is inserted into the conversation. The UI renders these as system messages indicating the integrity issue. + +**Location:** `ChatModel.kt:3565` -- `CIContent.RcvIntegrityError` + +--- + +### RULE-07: Decryption Error Surfacing + +**Invariant:** When a message cannot be decrypted, the app MUST display a `RcvDecryptionError` item showing the error type and count of affected messages. The app MUST NOT silently drop undecryptable messages. + +**Enforcement:** The Haskell core emits `CIContent.RcvDecryptionError(msgDecryptError, msgCount)` which the UI renders with an explanation and count. Ratchet re-synchronization can be triggered via `APISyncContactRatchet` / `APISyncGroupMemberRatchet`. + +**Location:** `ChatModel.kt:3566` -- `CIContent.RcvDecryptionError` + +--- + +### RULE-08: Delivery Receipt Consistency + +**Invariant:** Delivery receipt settings MUST be consistent: when a user enables/disables receipts globally, the change MUST propagate to all contacts/groups (optionally clearing per-chat overrides via `clearOverrides`). + +**Enforcement:** Global receipt toggle triggers `CC.SetAllContactReceipts(enable)`. Per-type settings use `CC.ApiSetUserContactReceipts` / `CC.ApiSetUserGroupReceipts` with `UserMsgReceiptSettings(enable, clearOverrides)`. The `privacyDeliveryReceiptsSet` preference gates the initial setup prompt shown during onboarding. + +**Location:** +- `SimpleXAPI.kt` -- `CC.SetAllContactReceipts`, `CC.ApiSetUserContactReceipts`, `CC.ApiSetUserGroupReceipts` +- `SimpleXAPI.kt` -- `ChatController.startChat()` -- triggers `setDeliveryReceipts` prompt + +--- + +### RULE-09: Chat Item TTL Enforcement + +**Invariant:** When a chat item TTL (time-to-live) is set globally or per-chat, expired messages MUST be deleted by the core. The app MUST NOT display expired items. + +**Enforcement:** Global TTL set via `CC.APISetChatItemTTL(userId, seconds)`. Per-chat TTL set via `CC.APISetChatTTL(userId, chatType, id, seconds)`. The Haskell core performs periodic cleanup. The current global TTL is stored in `ChatModel.chatItemTTL`. + +**Location:** `SimpleXAPI.kt` -- `CC.APISetChatItemTTL`, `CC.APISetChatTTL` + +--- + +## 3. Group Integrity + +### RULE-10: Role-Based Access Control + +**Invariant:** Group operations MUST respect the member's role. Only members with sufficient role level can perform privileged operations: +- **Owner:** can delete group, change any member's role, transfer ownership +- **Admin:** can add/remove members, change roles (up to Admin), create/delete group links +- **Moderator:** can delete other members' messages, block members +- **Member / Author / Observer:** cannot perform administrative actions + +**Enforcement:** The Haskell core validates role permissions server-side. The Kotlin UI layer uses `GroupMemberRole` comparisons (the enum is ordered: Observer < Author < Member < Moderator < Admin < Owner) to show/hide action buttons. + +**Location:** `ChatModel.kt:2369` -- `enum class GroupMemberRole`; various group management views + +--- + +### RULE-11: Group Member Removal Atomicity + +**Invariant:** When removing members from a group, the removal command MUST specify all member IDs atomically. Partial removal MUST NOT leave the group in an inconsistent state. + +**Enforcement:** `CC.ApiRemoveMembers(groupId, memberIds: List, withMessages: Boolean)` sends all member IDs in a single command. The `withMessages` flag controls whether the removed members' messages are also deleted. + +**Location:** `SimpleXAPI.kt` -- `CC.ApiRemoveMembers` + +--- + +### RULE-12: Group Link Role Default + +**Invariant:** When creating a group link, the default member role for joiners MUST be explicitly specified. The role can be updated after creation without regenerating the link. + +**Enforcement:** `CC.APICreateGroupLink(groupId, memberRole)` requires a role. `CC.APIGroupLinkMemberRole(groupId, memberRole)` updates it. The link itself remains stable. + +**Location:** `SimpleXAPI.kt` -- `CC.APICreateGroupLink`, `CC.APIGroupLinkMemberRole` + +--- + +### RULE-13: Member Blocking Scope + +**Invariant:** Blocking a member (`ApiBlockMembersForAll`) MUST apply the block for all group members (not just the requester). The `blocked` flag is visible to all members. Only roles >= Moderator can block. + +**Enforcement:** `CC.ApiBlockMembersForAll(groupId, memberIds, blocked)` sends the block/unblock to the core, which propagates it to all group members. + +**Location:** `SimpleXAPI.kt` -- `CC.ApiBlockMembersForAll`; `ChatModel.kt` -- `GroupMember.blockedByAdmin` + +--- + +## 4. File Transfer + +### RULE-14: File Encryption in Transit and at Rest + +**Invariant:** Files sent via XFTP MUST be encrypted before upload. Files received MUST be decrypted only after download. When `privacyEncryptLocalFiles` is enabled (default `true`), files stored locally MUST be encrypted with per-file keys (`CryptoFile.cryptoArgs`). + +**Enforcement:** The Haskell core handles XFTP encryption. Local file encryption is toggled via `CC.ApiSetEncryptLocalFiles(enable)`. The `CryptoFile` type carries optional `CryptoFileArgs` (key + nonce) for local decryption. Files are decrypted on-demand for display via `decryptCryptoFile()`. + +**Location:** +- `SimpleXAPI.kt` -- `CC.ApiSetEncryptLocalFiles`, `AppPreferences.privacyEncryptLocalFiles` +- `ChatModel.kt` -- `CryptoFile`, `CryptoFileArgs` +- `RecAndPlay.desktop.kt` -- `decryptCryptoFile()` usage in audio playback + +--- + +### RULE-15: Relay Approval for File Transfer + +**Invariant:** When `privacyAskToApproveRelays` is enabled (default `true`), the app MUST prompt the user before using XFTP relay servers suggested by contacts (as opposed to the user's own configured servers). The `userApprovedRelays` flag on `CC.ReceiveFile` records the user's consent. + +**Enforcement:** `CC.ReceiveFile(fileId, userApprovedRelays, encrypt, inline)` passes the approval flag. The UI prompts the user when the file is from an unapproved relay. + +**Location:** `SimpleXAPI.kt` -- `CC.ReceiveFile`, `AppPreferences.privacyAskToApproveRelays` + +--- + +## 5. Notification Delivery + +### RULE-16: Background Message Delivery (Android) + +**Invariant:** On Android, when `NotificationsMode.SERVICE` is selected (default), the app MUST maintain a foreground service (`SimplexService`) to ensure continuous message delivery. The service MUST survive app backgrounding and device sleep. When `NotificationsMode.PERIODIC` is selected, `MessagesFetcherWorker` MUST periodically wake and fetch messages. When `NotificationsMode.OFF`, no background delivery occurs. + +**Enforcement:** +- `SimplexService` runs as a foreground service with `START_STICKY` and a `WakeLock`. It displays a persistent notification on the `SIMPLEX_SERVICE_NOTIFICATION` channel. +- `MessagesFetcherWorker` is a `PeriodicWorkRequest` scheduled via `WorkManager`. +- The mode is stored in `AppPreferences.notificationsMode` and checked at app startup. + +**Location:** +- `android/src/main/java/chat/simplex/app/SimplexService.kt` +- `android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt` +- `SimpleXAPI.kt:7739` -- `enum class NotificationsMode` + +--- + +### RULE-17: Notification Preview Privacy + +**Invariant:** Notification content MUST respect `notificationPreviewMode`: +- `HIDDEN` -- notification shows no sender or message content +- `CONTACT` -- notification shows sender name only +- `MESSAGE` -- notification shows sender name and message preview + +**Enforcement:** `NtfManager` (Android) reads the preview mode from `AppPreferences.notificationPreviewMode` and constructs notifications accordingly. The `CallService` also respects this mode for call notifications (showing or hiding caller identity). + +**Location:** +- `android/src/main/java/chat/simplex/app/model/NtfManager.android.kt` -- `displayNotification()`, `notifyCallInvitation()` +- `android/src/main/java/chat/simplex/app/CallService.kt` -- `updateNotification()` +- `SimpleXAPI.kt` -- `AppPreferences.notificationPreviewMode` + +--- + +## 6. Call Integrity + +### RULE-18: Call Lifecycle Management + +**Invariant:** An active call MUST be properly managed across the full lifecycle: +1. **Incoming calls** MUST be reported via `CallManager.reportNewIncomingCall()` which triggers a notification (and on Android, a full-screen intent for lock-screen display). +2. **Only one call** can be active at a time. Accepting a new call MUST end any existing call first (`CallManager.acceptIncomingCall` checks `activeCall` and calls `endCall` if needed, guarded by `switchingCall` flag). +3. **Call state** MUST progress through defined states: `WaitCapabilities` -> `InvitationSent`/`InvitationAccepted` -> `OfferSent`/`OfferReceived` -> `Negotiated` -> `Connected` -> `Ended`. +4. **Call end** MUST clean up all resources: send `WCallCommand.End`, call `apiEndCall`, clear `activeCall`, cancel call notifications, and release platform resources. + +**Android enforcement:** +- `CallService` (foreground service) keeps the call alive in background with a `WakeLock` and ongoing notification on `CALL_SERVICE_NOTIFICATION` channel. +- `CallActivity` hosts the WebRTC WebView. +- Lock-screen behavior controlled by `AppPreferences.callOnLockScreen` (DISABLE / SHOW / ACCEPT). + +**Desktop enforcement:** +- Calls run in the system browser via the NanoWSD WebSocket server on `localhost:50395`. +- The `WebRTCController` composable manages the WebSocket lifecycle. +- On dispose, `WCallCommand.End` is sent and the server is stopped. + +**Location:** +- `common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt` +- `common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt` +- Android: `android/src/main/java/chat/simplex/app/CallService.kt`, `android/src/main/java/chat/simplex/app/views/call/CallActivity.kt` +- Desktop: `common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt` diff --git a/apps/multiplatform/product/views/call.md b/apps/multiplatform/product/views/call.md new file mode 100644 index 0000000000..51d323874c --- /dev/null +++ b/apps/multiplatform/product/views/call.md @@ -0,0 +1,115 @@ +# Audio / Video Call + +> **Related spec:** [spec/services/calls.md](../../spec/services/calls.md) + +## Purpose + +Make and receive end-to-end encrypted audio and video calls over WebRTC. The implementation differs significantly between Android (WebView-based with `CallActivity` and PiP support) and Desktop (browser-based WebRTC via NanoHTTPD server on localhost). + +## Route / Navigation + +- **Entry point (outgoing)**: Tap audio or video call button in `ChatInfoView` action buttons or `ChatView` toolbar +- **Entry point (incoming)**: `IncomingCallAlertView` banner appears at top of screen +- **Presented by**: `ActiveCallView()` (expect/actual composable) is shown when `chatModel.showCallView == true` +- **Dismiss**: Call ends when user taps end button or remote party disconnects; `callManager.endCall()` handles cleanup +- **Android PiP**: Call view supports picture-in-picture mode via `CallActivity` + +## Platform Differences + +| Aspect | Android | Desktop | +|---|---|---| +| WebRTC host | `WebView` with `WebViewAssetLoader` serving local assets | NanoHTTPD server on `localhost:50395` opened in system browser | +| Call activity | `CallActivity` (separate Android Activity) with lifecycle management | Inline composable with `WebRTCController` | +| PiP support | Native Android PiP via `CallActivity` | Not supported | +| Audio management | `CallAudioDeviceManager` with Android `AudioManager`, proximity wake lock | System browser audio routing | +| WebSocket | N/A | `NanoWSD` WebSocket server for bidirectional WebRTC signaling | + +## Page Sections + +### Incoming Call Banner (`IncomingCallAlertView`) + +Displayed as an overlay banner when `chatModel.activeCallInvitation` is set: + +| Element | Description | +|---|---| +| User profile image | Shown when multiple profiles exist (32dp `ProfileImage`) | +| Call type icon | `ic_videocam_filled` (green) for video, `ic_call_filled` (green) for audio | +| Call type text | `invitation.callTypeText` with caller info | +| Caller profile | `ProfilePreview` showing caller name and avatar (64dp) | +| Reject button | Red `ic_call_end_filled` icon -- ends the invitation via `callManager.endCall(invitation)` | +| Ignore button | Blue `ic_close` icon -- dismisses banner, cancels notification | +| Accept button | Green `ic_check_filled` icon -- accepts via `callManager.acceptIncomingCall(invitation)` | + +Sound: `SoundPlayer.start()` plays ringtone while banner is visible (unless call view is already showing). + +### Active Call View + +#### Android (`CallView.android.kt`) + +| Element | Description | +|---|---| +| WebView | `AndroidView` wrapping a `WebView` that loads `call.html` via `WebViewAssetLoader`; handles WebRTC JS bridge | +| `ActiveCallState` | Manages proximity lock (`PROXIMITY_SCREEN_OFF_WAKE_LOCK`), audio device manager, call sounds | +| Call controls overlay | Mic toggle, speaker toggle, camera switch, video toggle, end call button | +| Audio device selection | `CallAudioDeviceManager` with device enumeration (earpiece, speaker, Bluetooth, wired headset) | +| Permissions | Runtime permission checks for `CAMERA` and `RECORD_AUDIO` via Accompanist permissions library | + +#### Desktop (`CallView.desktop.kt`) + +| Element | Description | +|---|---| +| NanoHTTPD server | HTTP server on `localhost:50395` serving `call.html` and assets | +| NanoWSD WebSocket | WebSocket endpoint for bidirectional signaling between Kotlin and browser JS | +| `WebRTCController` | Processes `WCallCommand`/`WCallResponse` messages via `chatModel.callCommand` channel | +| Browser launch | `LocalUriHandler.openUri("http://localhost:50395/call.html")` opens system browser | +| Connection list | `connections: ArrayList` tracks active WebSocket connections | + +### WebRTC Signaling Flow + +| Step | Command/Response | Description | +|---|---|---| +| 1. Capabilities | `WCallResponse.Capabilities` | Local capabilities reported; `apiSendCallInvitation()` called | +| 2. Offer | `WCallResponse.Offer` | SDP offer + ICE candidates sent via `apiSendCallOffer()` | +| 3. Answer | `WCallResponse.Answer` | SDP answer + ICE candidates sent via `apiSendCallAnswer()` | +| 4. ICE | `WCallResponse.Ice` | Additional ICE candidates exchanged via `apiSendCallExtraInfo()` | +| 5. Connection | `WCallResponse.Connection` | WebRTC connection state changes; `CallState.Connected` set on success | +| 6. Connected | `WCallResponse.Connected` | Connection info (relay/direct) stored in `call.connectionInfo` | +| 7. PeerMedia | `WCallResponse.PeerMedia` | Remote party media source changes (mic, camera, screen) | +| 8. Media control | `WCallCommand.Media` | Toggle local media sources (mic, camera, screen audio/video) | +| 9. Camera switch | `WCallCommand.Camera` | Switch between front/back camera | +| 10. End | `WCallResponse.End` / `WCallResponse.Ended` | Call termination; cleanup and UI dismissal | + +### Call States (`CallState`) + +| State | Description | +|---|---| +| `WaitCapabilities` | Waiting for WebRTC capabilities | +| `InvitationSent` | Call invitation sent to remote party | +| `InvitationAccepted` | Callee accepted, starting WebRTC | +| `OfferSent` | SDP offer sent | +| `OfferReceived` | Callee received SDP offer | +| `AnswerReceived` | Caller received SDP answer | +| `Negotiated` | ICE negotiation complete | +| `Connected` | WebRTC media flowing; `connectedAt` timestamp set | +| `Ended` | Call terminated | + +### Call Sounds + +| Sound | Trigger | +|---|---| +| Connecting sound | `CallSoundsPlayer.startConnectingCallSound()` after invitation sent | +| In-call sound | `CallSoundsPlayer.startInCallSound()` when delivery receipt received | +| Ringtone | `SoundPlayer.start()` for incoming calls | +| End vibration | `CallSoundsPlayer.vibrate()` on call end (if was connected) | + +## Source Files + +| File | Path | +|---|---| +| `CallView.kt` | `views/call/CallView.kt` (common expect declarations) | +| `CallView.android.kt` | `androidMain/.../views/call/CallView.android.kt` | +| `CallView.desktop.kt` | `desktopMain/.../views/call/CallView.desktop.kt` | +| `IncomingCallAlertView.kt` | `views/call/IncomingCallAlertView.kt` | +| `CallManager.kt` | `views/call/CallManager.kt` | +| `WebRTC.kt` | `views/call/WebRTC.kt` | +| `CallAudioDeviceManager.kt` | `androidMain/.../views/call/CallAudioDeviceManager.kt` | diff --git a/apps/multiplatform/product/views/chat-list.md b/apps/multiplatform/product/views/chat-list.md new file mode 100644 index 0000000000..daa7907c5d --- /dev/null +++ b/apps/multiplatform/product/views/chat-list.md @@ -0,0 +1,136 @@ +# Chat List (Home Screen) + +> **Related spec:** [spec/client/chat-list.md](../../spec/client/chat-list.md) + +## Purpose + +Main screen of the SimpleX Chat Android and Desktop apps. Displays all conversations sorted by last activity, serves as the navigation root, and provides access to user profiles, settings, and new chat creation. + +## Route / Navigation + +- **Entry point**: App launch (root view), or back-navigation from any chat +- **Presented by**: `ChatListView` composable as the default view when `chatModel.chatId == null` +- **Navigation**: `ChatListNavLinkView` handles click routing to `ChatView` for each chat type +- **UserPicker**: Triggered by tapping the user avatar in the toolbar; presents `UserPicker` as a custom sheet (Android: bottom sheet overlay; Desktop: sidebar panel) + +## Platform Layout + +| Platform | Layout | +|---|---| +| Android | Single-column list; toolbar at top or bottom (one-hand UI); FAB for new chat | +| Desktop | 3-column layout: chat list (left), chat view (center), info/detail panel (right via `ModalManager.end`) | + +## Page Sections + +### Toolbar (`ChatListToolbar`) + +| Element | Location | Behavior | +|---|---|---| +| User avatar button | Leading | Opens `UserPicker` sheet (profile switcher, address, settings, preferences, connect to desktop/mobile) | +| "Your chats" title | Center | Tappable to scroll list to top | +| Connection status indicator (`SubscriptionStatusIndicator`) | Adjacent to title | Shows SMP server subscription status; taps open `ServersSummaryView` | +| New chat button (pencil icon) | Trailing (one-hand UI) or FAB (standard) | Opens `NewChatSheet` modal via `showNewChatSheet()` | +| Active call indicator | Trailing (Desktop, one-hand UI) | `ActiveCallInteractiveArea` shown when a call is active | +| Updating progress | Trailing | Shows progress circle/indicator during database updates | +| Stopped indicator | Trailing | Red warning icon when chat engine is stopped | + +The toolbar supports two layout modes controlled by `appPrefs.oneHandUI`: +- **Standard (top)**: `DefaultAppBar` at top with `NavigationButtonMenu` leading, title center, buttons trailing. FAB at bottom-right for new chat. +- **One-hand UI (bottom)**: Toolbar at bottom of screen with `Column(Modifier.align(Alignment.BottomCenter))`; list rendered with `reverseLayout = true`; no FAB (new chat button is inline in toolbar). + +### Search Bar (`ChatListSearchBar`) + +| Element | Description | +|---|---| +| Search icon | Magnifying glass icon at leading edge | +| Text field | `SearchTextField` with placeholder "Search or paste SimpleX link" | +| Filter button | `ToggleFilterEnabledButton` (filter icon) toggles unread-only filter; shown when search text is empty | +| Clear button | Appears when text is entered; `BackHandler` clears search on back | + +Behavior: +- Filters chat list in real-time by contact/group name via `filteredChats()` +- Detects pasted SimpleX links (`strHasSingleSimplexLink`) and triggers `planAndConnect()` connection dialogue +- In one-hand UI mode, search bar appears below tag filters with IME spacer; in standard mode, above tag filters + +### Chat Filter Tags (`TagsView`) + +Managed by `chatModel.userTags`, `chatModel.presetTags`, and `chatModel.activeChatTagFilter`: + +| Filter | `PresetTagKind` | Icon | Description | +|---|---|---|---| +| Group Reports | `GROUP_REPORTS` | Flag | Chats with moderation reports (non-collapsible) | +| Favorites | `FAVORITES` | Star | User-favorited chats | +| Contacts | `CONTACTS` | Person | Direct contacts and contact requests | +| Groups | `GROUPS` | Group | Group conversations (non-business) | +| Business | `BUSINESS` | Work | Business chat conversations | +| Notes | `NOTES` | Folder | Notes to self | +| Custom tags | `UserTag(ChatTag)` | Label/emoji | User-created tags with custom emoji and name | +| Unread | `ActiveFilter.Unread` | Filter list icon | Chats with unread messages (toggle via filter button) | + +Display logic: +- When collapsible preset tags exceed 3 total (with user tags), they collapse into a `CollapsedTagsFilterView` dropdown menu +- Non-collapsible tags (`GROUP_REPORTS`) always show expanded +- User tags show with emoji or label icon; long-press opens `TagsDropdownMenu` (edit, delete, change order) +- "+" button at end opens `TagListEditor` for creating new tags + +### Chat Preview Rows (`ChatPreviewView`) + +Each row rendered by `ChatPreviewView` inside `ChatListNavLinkView`: + +| Element | Description | +|---|---| +| Avatar | `ProfileImage` with overlay icons (inactive contact, left/removed group member) | +| Chat name | Display name with verified icon for verified contacts; colored for pending/connecting states | +| Last message preview | Truncated text of most recent message; draft indicator with edit icon; attachment icons | +| Timestamp | Relative time of last activity | +| Unread badge | Numeric count badge; distinct styling for mentions | +| Muted indicator | Bell-off icon when notifications are muted | +| Favorite indicator | Star icon for favorited chats | +| Incognito indicator | Shows when connected via incognito profile | +| Connection status | Shows connecting/pending state for incomplete connections | + +Chat types handled by `ChatListNavLinkView`: +- `ChatInfo.Direct` -- direct contact chat +- `ChatInfo.Group` -- group chat (with in-progress indicator for joining) +- `ChatInfo.Local` -- note-to-self folder +- `ChatInfo.ContactRequest` -- incoming contact request (tap shows accept/reject alert) +- `ChatInfo.ContactConnection` -- pending connection (tap opens `ContactConnectionView`) + +### Context Menu (Long Press / Right Click) + +Each chat type provides specific dropdown menu items: + +| Chat Type | Menu Items | +|---|---| +| Direct contact | Mark read/unread, toggle favorite, toggle notify, tag list, clear chat, delete contact | +| Group | Mark read/unread, toggle favorite, toggle notify, tag list, clear chat, archive all reports (moderator, when reports exist), leave group, delete group | +| Note folder | Mark read/unread, clear notes | +| Contact request | Accept, reject | +| Contact connection | Set name/alias, delete | + +### Floating Elements + +| Element | Condition | Description | +|---|---|---| +| One-hand UI card (`ToggleChatListCard`) | `oneHandUICardShown == false` | Dismissible card introducing bottom toolbar mode with toggle switch | +| Address creation card (`AddressCreationCard`) | `addressCreationCardShown == false` | Prompts user to create a SimpleX address; tappable card opens `UserAddressLearnMore` | +| FAB (new chat button) | Standard mode, search empty, chat running | `FloatingActionButton` at bottom-right, pencil icon, opens `NewChatSheet` | + +### Empty States + +| State | Display | +|---|---| +| Loading | "Loading chats..." centered text | +| No chats | "You have no chats" centered text | +| No filtered chats | "No chats in list [tag name]" or "No unread chats" with clickable filter reset | +| No search results | "No chats found" centered text | + +## Source Files + +| File | Path | +|---|---| +| `ChatListView.kt` | `views/chatlist/ChatListView.kt` | +| `ChatListNavLinkView.kt` | `views/chatlist/ChatListNavLinkView.kt` | +| `ChatPreviewView.kt` | `views/chatlist/ChatPreviewView.kt` | +| `UserPicker.kt` | `views/chatlist/UserPicker.kt` | +| `TagListView.kt` | `views/chatlist/TagListView.kt` | diff --git a/apps/multiplatform/product/views/chat.md b/apps/multiplatform/product/views/chat.md new file mode 100644 index 0000000000..64abda7ee6 --- /dev/null +++ b/apps/multiplatform/product/views/chat.md @@ -0,0 +1,135 @@ +# Chat View (Conversation) + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Purpose + +Full conversation view for displaying and interacting with messages in a direct contact chat, group chat, or note-to-self. Supports text messaging with markdown, media attachments, voice messages, E2E encrypted calls, message reactions, replies, forwarding, reporting, and content search/filtering. + +## Route / Navigation + +- **Entry point**: Tap a chat row in `ChatListView` (routed by `ChatListNavLinkView`) +- **Presented by**: `ChatView` composable bound to `chatModel.chatId`; on Desktop, shown in the center column +- **Back navigation**: Sets `chatModel.chatId = null`, stops `AudioPlayer`, clears group members, returns to chat list +- **Sub-navigation**: + - Info button opens `ChatInfoView` (contact) or `GroupChatInfoView` (group) via `ModalManager.end` + - Member avatars in group chats navigate to `GroupMemberInfoView` + - Reports button opens `GroupReportsView` for groups with moderation reports + - Support chats button opens `MemberSupportView` (moderators) or member support chat (regular members) + +## Page Sections + +### Navigation Bar (`ChatLayout`) + +Custom toolbar with themed background: + +| Element | Description | +|---|---| +| Back button | Returns to chat list; stops audio/video playback | +| Contact/Group avatar | Small profile image in toolbar | +| Chat name | Display name; tappable to open info view | +| Verified shield | Shows verified contact checkmark (direct chats with verified contacts only) | +| More menu button | Opens overflow menu containing search and audio/video call buttons (call buttons shown in direct chats only) | +| Info button | Opens `ChatInfoView` (direct) or `GroupChatInfoView` (group) | +| Reports count | Badge for group reports count; taps open reports view | +| Support chats | Badge for member support; taps open support chat view | + +### Message List + +Rendered by `LazyColumnWithScrollBar` with pagination: + +| Feature | Description | +|---|---| +| Scroll direction | Bottom-to-top (newest messages at bottom) | +| Pagination | `apiLoadMessages` called on scroll to load more; supports `.before`, `.after`, `.around`, `.initial` | +| Merged items | Adjacent messages grouped with `ItemSeparation` (timestamp, large gap, date separators) | +| Floating buttons | Scroll-to-bottom button with unread count | +| Date separators | Date headers between messages from different days | +| Wallpaper | Per-chat themed background via `perChatTheme` from contact/group `uiThemes` | +| Content filter | Filter messages by type via `ContentFilter` (images, files, links, etc.) | + +### Message Types + +Each type has a dedicated composable in `views/chat/item/`: + +| Type | Composable | Description | +|---|---|---| +| Text | `FramedItemView` | Rendered with markdown (bold, italic, code, links, `@mentions`) via `CIMarkdownText` | +| Image | `CIImageView` | Thumbnail with tap-to-fullscreen via `ImageFullScreenView` | +| Video | `CIVideoView` | Video thumbnail with play button; inline playback via `VideoPlayerHolder` | +| Voice | `CIVoiceView` | Waveform visualization with playback controls and duration | +| File | `CIFileView` | File icon, name, size; download/open actions with progress indicator | +| Link preview | `ChatItemLinkView` | URL preview card with title, description, image (defined in `LinkPreviews.kt`) | +| Emoji-only | `EmojiItemView` | Large emoji rendering without message bubble | +| Call event | `CICallItemView` | Call status (missed, ended, duration) | +| Group event | `CIEventView` | Member joined/left, role changes, group updates | +| E2EE info | `CIChatFeatureView` | Encryption status and feature change notifications | +| Group invitation | `CIGroupInvitationView` | Inline group join invitation card | +| Deleted | `DeletedItemView` / `MarkedDeletedItemView` | Placeholder for deleted messages | +| Decryption error | `CIRcvDecryptionError` | Error with ratchet sync suggestion | +| Invalid JSON | `CIInvalidJSONView` | Developer fallback for malformed items | +| Integrity error | `IntegrityErrorItemView` | Message integrity/gap warnings | + +### Message Interactions + +Long-press context menu on any message: + +| Action | Description | +|---|---| +| Reply | Sets compose bar to reply mode with quoted message (`ComposeContextItem.QuotedItem`) | +| Forward | Opens destination picker; uses `apiPlanForwardChatItems` with confirmation for partial forwards | +| Copy | Copies message text to clipboard | +| Edit | Enters edit mode (`ComposeContextItem.EditingItem`); own messages within edit window | +| Delete | Delete for self or delete for everyone (with confirmation via `deleteMessagesAlertDialog`) | +| Moderate | Group moderators can delete messages for all members (`moderateMessagesAlertDialog`) | +| React | Emoji reaction picker | +| Report | Report message to group moderators (`ComposeContextItem.ReportedItem` with `ReportReason`) | +| Select multiple | Enters multi-select mode (`selectedChatItems`) with bulk delete/forward/archive toolbar | +| Archive | Archive selected reports (moderators) | + +### Compose Bar (`ComposeView` + `SendMsgView`) + +Bottom input area for composing messages: + +| Element | Description | +|---|---| +| Text field | `PlatformTextField` with markdown support, `@mention` autocomplete, file paste support | +| Attachment button | Opens `ModalBottomSheetLayout` with options: camera, gallery (image/video), file | +| Send button | Sends message; changes to checkmark for reports; animated size/alpha | +| Voice record button | Shown when text is empty and voice allowed; hold to record, release to preview | +| Live message button | Start/update live typing message (if `liveMessageAlertShown`) | +| Context preview | Shows quoted message, editing indicator, or forwarding source above text field | +| Media preview | Thumbnail row of selected images/videos before sending | +| Link preview | Auto-generated link preview card (`ComposePreview.CLinkPreview`) | +| Connecting status | "Connecting..." text shown when contact is not yet ready | +| Commands menu | Developer commands (`showCommandsMenu`) | + +Compose states (`ComposeState`): +- `NoContextItem` -- normal new message +- `QuotedItem` -- replying to a message +- `EditingItem` -- editing own message +- `ForwardingItems` -- forwarding from another chat +- `ReportedItem` -- reporting a message with reason + +### Multi-Select Toolbar (`SelectedItemsButtonsToolbar`) + +Shown when `selectedChatItems != null`: + +| Button | Description | +|---|---| +| Delete / Archive | Delete selected messages (for self, or for everyone if allowed by `fullDeleteAllowed`); shown as Archive for report items (group moderators only) | +| Forward | Forward selected messages to another chat | +| Moderate | Delete selected messages for all members (group moderators only) | + +### Timed/Disappearing Messages + +When `timedMessageAllowed` is true, compose bar includes a timer icon for setting message disappear time via `customDisappearingMessageTimePref`. + +## Source Files + +| File | Path | +|---|---| +| `ChatView.kt` | `views/chat/ChatView.kt` | +| `ComposeView.kt` | `views/chat/ComposeView.kt` | +| `SendMsgView.kt` | `views/chat/SendMsgView.kt` | +| Chat item views | `views/chat/item/*.kt` | diff --git a/apps/multiplatform/product/views/contact-info.md b/apps/multiplatform/product/views/contact-info.md new file mode 100644 index 0000000000..32793a3b70 --- /dev/null +++ b/apps/multiplatform/product/views/contact-info.md @@ -0,0 +1,104 @@ +# Contact Info + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Purpose + +View contact details, manage per-contact preferences, verify security codes for E2E encryption, manage connection settings (switch address, sync ratchet), and perform destructive actions like clearing or deleting a contact. + +## Route / Navigation + +- **Entry point**: Tap the info button in `ChatView` navigation bar (when viewing a direct contact chat) +- **Presented by**: `ChatInfoView` composable shown via `ModalManager.end` from `ChatView` +- **Sub-navigation**: + - Contact preferences -> `ContactPreferencesView` (via `ModalManager.end`) + - Security code verification -> `VerifyCodeView` (via `ModalManager.end`) + - Chat wallpaper -> wallpaper editor + - Group profile view (for group-direct contacts) + +## Page Sections + +### Contact Info Header + +| Element | Description | +|---|---| +| Profile image | Large circular avatar (tappable) | +| Display name | Contact's display name | +| Full name | Optional full name below display name | +| Connection status | Shows if contact is ready, connecting, or has issues | + +### Local Alias + +Editable text field for setting a local-only name visible only on this device. Not shared with the contact. Changes saved via `setContactAlias()`. + +### Action Buttons + +Horizontal row of quick-action buttons: + +| Button | Description | +|---|---| +| Search | Triggers `onSearchClicked` to search messages in chat | +| Audio call | Initiate audio call | +| Video call | Initiate video call | +| Mute/Unmute | Toggle notification mode | + +### Incognito Section + +Shown only when `customUserProfile` is set (connected via incognito profile): + +| Element | Description | +|---|---| +| Incognito icon | Indicates incognito connection | +| Profile name | The random profile name used for this connection | + +### Chat Preferences + +| Setting | Description | +|---|---| +| Send receipts | Per-contact delivery receipt setting (`SendReceipts` tristate: default/on/off) | +| Chat item TTL | Per-contact message retention setting (`ChatItemTTL` with alert confirmation) | +| Contact preferences | Opens `ContactPreferencesView` for feature toggles (timed messages, full delete, reactions, voice, calls) | + +### Connection Details + +Shown when `connectionStats` is available: + +| Element | Description | +|---|---| +| Connection stats | Server information, agent connection ID | +| Switch address | Initiates SMP server address switch (`apiSwitchContact`) with confirmation alert | +| Abort switch | Cancels an in-progress address switch (`apiAbortSwitchContact`) | +| Sync connection | Fixes encryption ratchet synchronization (`apiSyncContactRatchet`) | +| Force sync | Force ratchet re-synchronization with confirmation alert | + +### Security Code Verification + +| Element | Description | +|---|---| +| Verify button | Opens `VerifyCodeView` showing the connection security code | +| Verified badge | Shows checkmark when contact is verified | +| Code comparison | Side-by-side code display for out-of-band verification via `apiVerifyContact` | + +### Developer Tools Section + +Shown when `developerTools` preference is enabled: + +| Element | Description | +|---|---| +| Database ID | Contact's internal database identifier | +| Agent connection ID | Underlying SMP agent connection ID | + +### Destructive Actions + +| Action | Description | +|---|---| +| Clear chat | Deletes all messages in chat (with confirmation via `clearChatDialog`) | +| Delete contact | Removes the contact and all associated data (with confirmation via `deleteContactDialog`) | + +## Source Files + +| File | Path | +|---|---| +| `ChatInfoView.kt` | `views/chat/ChatInfoView.kt` | +| `ContactPreferences.kt` | `views/chat/ContactPreferences.kt` | +| `VerifyCodeView.kt` | `views/chat/VerifyCodeView.kt` | diff --git a/apps/multiplatform/product/views/group-info.md b/apps/multiplatform/product/views/group-info.md new file mode 100644 index 0000000000..65b068adc8 --- /dev/null +++ b/apps/multiplatform/product/views/group-info.md @@ -0,0 +1,145 @@ +# Group Chat Info + +> **Related spec:** [spec/client/chat-view.md](../../spec/client/chat-view.md) + +## Purpose + +View and manage group settings, member list, group preferences, group links, member admission, welcome messages, and moderation features. The scope of available actions depends on the user's role within the group (member, moderator, admin, owner). + +## Route / Navigation + +- **Entry point**: Tap the info button in `ChatView` navigation bar (when viewing a group chat) +- **Presented by**: `GroupChatInfoView` composable shown via `ModalManager.end` from `ChatView` +- **Sub-navigation**: + - Edit group profile -> `GroupProfileView` (via `ModalManager.end`) + - Add members -> `AddGroupMembersView` (via `ModalManager.end`) + - Group link -> `GroupLinkView` (via `ModalManager.end`) + - Group preferences -> `GroupPreferencesView` (via `ModalManager.end`) + - Welcome message -> `GroupWelcomeView` (via `ModalManager.end`) + - Member info -> `GroupMemberInfoView` (via `ModalManager.end`) + - Chat wallpaper -> wallpaper editor + - Member support -> `MemberSupportView` (via `ModalManager.end`) + +## Page Sections + +### Group Info Header + +| Element | Description | +|---|---| +| Group image | Large circular profile image | +| Group name | Display name (editable by owners via `GroupProfileView`) | +| Member count | "N members" label from `activeSortedMembers` | +| Full name | Optional secondary name | +| Description | Group description text (if set) | + +### Local Alias + +Editable text field for a local-only alias (not shared with other members). Changes saved via `setGroupAlias()`. + +### Action Buttons + +Horizontal row of action buttons: + +| Button | Description | +|---|---| +| Search | Triggers `onSearchClicked` callback to search messages in chat | +| Mute/Unmute | Toggle notification mode | +| Add members | Opens `AddGroupMembersView` (shown when user has admin+ role and `groupInfo.canAddMembers`) | + +### Group Management Section + +Available actions depend on role (`GroupMemberRole`): + +| Action | Minimum Role | Description | +|---|---|---| +| Edit group profile | Owner | Opens `GroupProfileView` to edit name, image, description | +| Add members | Admin | Opens `AddGroupMembersView` to invite contacts | +| Manage group link | Admin | Opens `GroupLinkView` to create/share/delete group link | +| Member support | Moderator | Opens `MemberSupportView` to manage member support chats | +| Edit welcome message | Owner | Opens `GroupWelcomeView` to set the auto-sent welcome text | +| Group preferences | Any | Opens `GroupPreferencesView` (read-only; only owners can change settings) | + +### Chat Preferences + +| Setting | Description | +|---|---| +| Send receipts | Per-group delivery receipt setting (`SendReceipts`); limited to groups under `SMALL_GROUPS_RCPS_MEM_LIMIT` (20 members) | +| Chat item TTL | Per-group message retention setting with confirmation alert via `setChatTTLAlert` | + +### Member List + +Displays `activeSortedMembers` (excluding left/removed members, sorted by role descending): + +| Element | Description | +|---|---| +| Member avatar | `MEMBER_ROW_AVATAR_SIZE` (42dp) profile image | +| Member name | Display name with role badge | +| Member role | Owner, Admin, Moderator, Member, Observer | +| Member status | Active, connecting, pending, left, removed | +| Tap action | Opens `GroupMemberInfoView` with connection stats and verification code | + +### Group Link (`GroupLinkView`) + +| Element | Description | +|---|---| +| Create link button | `apiCreateGroupLink` generates a shareable group invitation link | +| QR code display | QR code rendering of the group link | +| Short link toggle | Switch between short and full link display | +| Share button | System share for the link | +| Copy button | Copy link to clipboard | +| Member role selector | Set the default role for members joining via link (`acceptMemberRole`) | +| Add short link | `apiAddGroupShortLink` creates a short link that includes group profile | +| Delete link | Remove the group link with confirmation | + +### Add Members (`AddGroupMembersView`) + +| Element | Description | +|---|---| +| Contact list | Filterable list of contacts to invite | +| Role selector | Set the role for invited members | +| Invite button | Sends group invitations to selected contacts | +| Group link option | Alternative to direct invitation | + +### Group Member Info (`GroupMemberInfoView`) + +| Element | Description | +|---|---| +| Member profile | Avatar, name, role | +| Connection stats | Server information, connection status | +| Security code | Verification code for the member connection | +| Role change | Change member role (admin+ only) | +| Remove member | Remove from group (admin+ only) | +| Block member | Block member for self | +| Direct message | Open direct chat with member | + +### Developer Tools Section + +Shown when `developerTools` preference is enabled: + +| Element | Description | +|---|---| +| Database ID | Group's internal database identifier | + +### Destructive Actions + +| Action | Condition | Description | +|---|---|---| +| Clear chat | Any member | Deletes all messages locally (`clearChatDialog`) | +| Leave group | Non-owner | Leave the group (`leaveGroupDialog`) | +| Delete group | Owner or non-current member | Delete group for all (owner) or for self (`deleteGroupDialog`) | + +Business chats use alternative labels: "Delete chat" instead of "Delete group". + +## Source Files + +| File | Path | +|---|---| +| `GroupChatInfoView.kt` | `views/chat/group/GroupChatInfoView.kt` | +| `GroupMemberInfoView.kt` | `views/chat/group/GroupMemberInfoView.kt` | +| `AddGroupMembersView.kt` | `views/chat/group/AddGroupMembersView.kt` | +| `GroupLinkView.kt` | `views/chat/group/GroupLinkView.kt` | +| `GroupProfileView.kt` | `views/chat/group/GroupProfileView.kt` | +| `GroupPreferences.kt` | `views/chat/group/GroupPreferences.kt` | +| `WelcomeMessageView.kt` | `views/chat/group/WelcomeMessageView.kt` | +| `MemberAdmission.kt` | `views/chat/group/MemberAdmission.kt` | +| `MemberSupportView.kt` | `views/chat/group/MemberSupportView.kt` | diff --git a/apps/multiplatform/product/views/new-chat.md b/apps/multiplatform/product/views/new-chat.md new file mode 100644 index 0000000000..b664fda67f --- /dev/null +++ b/apps/multiplatform/product/views/new-chat.md @@ -0,0 +1,96 @@ +# New Chat / Connection + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) + +## Purpose + +Create new contacts, groups, or connect with others via one-time invitation links or by scanning/pasting SimpleX links. This is the primary entry point for establishing new E2E encrypted connections. + +## Route / Navigation + +- **Entry point**: Tap the new chat button (pencil icon) in `ChatListView` toolbar or FAB +- **Presented by**: `NewChatSheet` modal from `ChatListView` via `showNewChatSheet()`; wraps `NewChatView` and group creation in `ModalManager.start` +- **Internal navigation**: `NewChatSheet` provides 3 action buttons: + - "Create 1-time link" -- opens `NewChatView` with `INVITE` tab (generate and share a one-time invitation link) + - "Scan / paste link" -- opens `NewChatView` with `CONNECT` tab (scan QR code or paste a received link) + - "Create group" -- opens `AddGroupView` +- **Tabs within NewChatView**: `HorizontalPager` with `TabRow` toggles between `NewChatOption.INVITE` (1-time link) and `NewChatOption.CONNECT` (connect via link) +- **Swipe gesture**: Left/right swipe switches between tabs (Android only; `userScrollEnabled = appPlatform.isAndroid`) +- **Dismiss behavior**: On dispose, a `DisposableEffect` shows an alert dialog (via `AlertManager.shared.showAlertDialog`) asking whether to keep an unused invitation link or delete it via `controller.deleteChat()` + +## Page Sections + +### Tab Selector + +| Tab | Icon | Label | Description | +|---|---|---|---| +| 1-time link | `ic_repeat_one` | "1-time link" | Generate and share a one-time invitation link | +| Connect via link | `ic_qr_code` | "Connect via link" | Scan QR code or paste a received link | + +### Invite Tab (1-time Link) -- `PrepareAndInviteView` + +Displayed when `selection == INVITE`: + +| Element | Description | +|---|---| +| QR code display | Generated QR code for the invitation link (`SimpleXLinkQRCode`) | +| Short/full link toggle | Switch between short and full link display | +| Share button | System share for the invitation link | +| Copy button | Copy link to clipboard | +| Incognito toggle | Option to connect with a random profile | +| Loading state | `CreatingLinkProgressView` with "Creating link" text while `creatingConnReq` is true | +| Retry button | `RetryButton` shown if link creation fails; calls `createInvitation()` | + +Link creation calls `apiAddContact` which returns a `CreatedConnLink` with both `connFullLink` and optional `connShortLink`. The invitation is tracked via `chatModel.showingInvitation`. + +### Connect Tab -- `ConnectView` + +Displayed when `selection == CONNECT`: + +| Element | Description | +|---|---| +| QR code scanner | Camera-based QR code scanner (`showQRCodeScanner` state) | +| Paste link field | Text field for pasting a SimpleX link (`pastedLink`) | +| Connect button | Initiates connection via `planAndConnect()` | + +When a valid SimpleX link is detected: +1. `planAndConnect()` is called with the link URI +2. If the link matches a known contact, filters to that chat +3. If the link matches a known group, filters to that group +4. Otherwise, creates a new connection + +### Create Group (`AddGroupView`) + +| Element | Description | +|---|---| +| Group name field | Required display name input with `FocusRequester` | +| Profile image picker | `GetImageBottomSheet` for selecting/cropping a group avatar | +| Incognito toggle | Option to create group with random profile (`incognitoPref`) | +| Create button | Calls `apiNewGroup()`, then opens `AddGroupMembersView` (normal) or `GroupLinkView` (incognito) | + +Group creation flow: +1. User enters group name and optionally selects an image +2. `apiNewGroup()` creates the group and returns `GroupInfo` +3. `openGroupChat()` navigates to the new group chat +4. `setGroupMembers()` preloads member data +5. `AddGroupMembersView` opens for inviting contacts (or `GroupLinkView` for incognito groups) + +### QR Code Components (`QRCode.kt`) + +| Component | Description | +|---|---| +| `SimpleXLinkQRCode` | Renders a QR code for a SimpleX connection link | +| QR scanner | Platform camera scanner for reading QR codes | +| Short link display | Compact link text with copy/share actions | + +## Source Files + +| File | Path | +|---|---| +| `NewChatView.kt` | `views/newchat/NewChatView.kt` | +| `AddGroupView.kt` | `views/newchat/AddGroupView.kt` | +| `QRCode.kt` | `views/newchat/QRCode.kt` | +| `NewChatSheet.kt` | `views/newchat/NewChatSheet.kt` | +| `ConnectPlan.kt` | `views/newchat/ConnectPlan.kt` | +| `QRCodeScanner.kt` | `views/newchat/QRCodeScanner.kt` (expect/actual) | +| `ContactConnectionInfoView.kt` | `views/newchat/ContactConnectionInfoView.kt` | diff --git a/apps/multiplatform/product/views/onboarding.md b/apps/multiplatform/product/views/onboarding.md new file mode 100644 index 0000000000..4127ac65f7 --- /dev/null +++ b/apps/multiplatform/product/views/onboarding.md @@ -0,0 +1,139 @@ +# Onboarding + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) + +## Purpose + +First-time setup flow for new users. Guides through app introduction, profile creation, database passphrase setup (Desktop), server operator conditions acceptance, SimpleX address creation, and notification configuration (Android). Also provides an entry point for device migration. + +## Route / Navigation + +- **Entry point**: App launch when `onboardingStage` is not `OnboardingComplete` +- **Presented by**: `OnboardingView` renders the appropriate step based on `OnboardingStage` enum +- **Flow direction**: Linear progression controlled by `appPrefs.onboardingStage` +- **Completion**: Sets `onboardingStage` to `OnboardingComplete` + +## Onboarding Stages + +The `OnboardingStage` enum defines the flow: + +| Stage | Description | +|---|---| +| `Step1_SimpleXInfo` | Welcome screen with app introduction | +| `Step2_CreateProfile` | Create first user profile | +| `LinkAMobile` | Desktop-only: link a mobile device | +| `Step2_5_SetupDatabasePassphrase` | Desktop-only: set database encryption passphrase | +| `Step3_ChooseServerOperators` | Accept server operator conditions | +| `Step3_CreateSimpleXAddress` | Create a SimpleX contact address | +| `Step4_SetNotificationsMode` | Android-only: configure notification mode | +| `OnboardingComplete` | Onboarding finished | + +## Page Sections + +### Step 1: Welcome / SimpleX Info (`SimpleXInfo`) + +**Stage**: `Step1_SimpleXInfo` + +| Element | Description | +|---|---| +| Logo | `SimpleXLogo` -- SimpleX Chat logo (light/dark variant based on `isInDarkTheme()`) | +| Info button | `OnboardingInformationButton` -- "The next generation of private messaging"; taps open `HowItWorks` fullscreen modal | +| Privacy redefined | `InfoRow` with privacy icon: "No user identifiers" | +| Immune to spam | `InfoRow` with shield icon: "You decide who can connect" | +| Decentralized | `InfoRow` with decentralized icon: "Anybody can host servers" | +| **Create your profile** button | `OnboardingActionButton` -- primary action; advances to profile creation | +| **Migrate from another device** button | `TextButtonBelowOnboardingButton` -- opens `MigrateToDeviceView` fullscreen modal | + +Layout: `ColumnWithScrollBar` with `DEFAULT_ONBOARDING_HORIZONTAL_PADDING`, max width constrained (250dp Android, 500dp Desktop). + +### Step 2: Create Profile + +**Stage**: `Step2_CreateProfile` + +| Element | Description | +|---|---| +| Display name field | Required text input; auto-focused | +| Validation | Name validation with `mkValidName` check | +| Create button | Creates profile via API; advances to next step | + +Profile is stored locally and only shared with contacts. + +### Step 2.5: Setup Database Passphrase (Desktop only) + +**Stage**: `Step2_5_SetupDatabasePassphrase` + +| Element | Description | +|---|---| +| Passphrase field | Secure text input for database encryption key | +| Confirm field | Passphrase confirmation | +| Set button | Encrypts database with passphrase | + +### Link a Mobile (Desktop only) + +**Stage**: `LinkAMobile` + +| Element | Description | +|---|---| +| Instructions | How to connect mobile device to desktop | +| QR code | Connection QR code for mobile scanning | +| Skip button | Skip this step | + +### Step 3: Choose Server Operators + +**Stage**: `Step3_ChooseServerOperators` + +| Element | Description | +|---|---| +| Operator list | Available server operators with conditions | +| Conditions text | Terms of service for selected operators | +| Accept button | Accept conditions and continue | + +Managed by `ChooseServerOperators.kt`. + +### Step 3b: Create SimpleX Address + +**Stage**: `Step3_CreateSimpleXAddress` + +| Element | Description | +|---|---| +| Address creation | Auto-creates a SimpleX contact address | +| QR code | Displays the created address as QR code | +| Share button | Share address link | +| Skip button | Skip address creation | + +### Step 4: Set Notifications Mode (Android only) + +**Stage**: `Step4_SetNotificationsMode` + +| Element | Description | +|---|---| +| Notification options | Instant (background service) / Periodic (every 10 min) / Off | +| Description | Explains battery impact and notification behavior for each mode | +| Continue button | Saves selection and completes onboarding | + +Managed by `SetNotificationsMode.kt`. + +### What's New (`WhatsNewView`) + +Shown after onboarding or when triggered from Settings: + +| Element | Description | +|---|---| +| Version highlights | New features and changes in the current version | +| Updated conditions | Notice about updated server operator conditions (if applicable) | +| Close button | Dismisses the view | + +Triggered in `ChatListView` via `shouldShowWhatsNew()` with a 1-second delay. + +## Source Files + +| File | Path | +|---|---| +| `OnboardingView.kt` | `views/onboarding/OnboardingView.kt` | +| `SimpleXInfo.kt` | `views/onboarding/SimpleXInfo.kt` | +| `HowItWorks.kt` | `views/onboarding/HowItWorks.kt` | +| `SetupDatabasePassphrase.kt` | `views/onboarding/SetupDatabasePassphrase.kt` | +| `SetNotificationsMode.kt` | `views/onboarding/SetNotificationsMode.kt` | +| `ChooseServerOperators.kt` | `views/onboarding/ChooseServerOperators.kt` | +| `WhatsNewView.kt` | `views/onboarding/WhatsNewView.kt` | +| `LinkAMobileView.kt` | `views/onboarding/LinkAMobileView.kt` | diff --git a/apps/multiplatform/product/views/settings.md b/apps/multiplatform/product/views/settings.md new file mode 100644 index 0000000000..e668bf2d04 --- /dev/null +++ b/apps/multiplatform/product/views/settings.md @@ -0,0 +1,159 @@ +# Settings + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) | [spec/services/theme.md](../../spec/services/theme.md) | [spec/services/notifications.md](../../spec/services/notifications.md) + +## Purpose + +Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and developer tools. Accessed from the UserPicker or directly from the chat list toolbar. + +## Route / Navigation + +- **Entry point**: Tap user avatar in `ChatListView` toolbar -> `UserPicker` -> Settings option; or directly via `NavigationButtonMenu` when no users exist +- **Presented by**: `SettingsView` composable via `ModalManager.start.showModalCloseable` +- **Navigation title**: "Your settings" (`AppBarTitle`) +- **Sub-navigation**: Each settings row opens a dedicated view via `showSettingsModal` or `showCustomModal` + +## Platform Differences + +| Aspect | Android | Desktop | +|---|---|---| +| App section | Device settings, app version | App updates (`AppUpdater`), device settings, app version | +| Notifications | Full notification mode selection (instant/periodic/off) | Notification settings | +| Use from desktop/mobile | "Use from desktop" option in UserPicker | "Link a mobile" / "Linked mobiles" option in UserPicker | +| Database migration | "Migrate to another device" with auth | Same | + +## Page Sections + +### Settings Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| Notifications | `ic_bolt` / `ic_bolt_off` | `NotificationsSettingsView` | Push notification mode and preview settings | +| Network & servers | `ic_wifi_tethering` | `NetworkAndServersView` | SMP/XFTP servers, proxy, .onion hosts, advanced network | +| Audio & video calls | `ic_videocam` | `CallSettingsView` | WebRTC relay policy, ICE servers | +| Privacy & security | `ic_lock` | `PrivacySettingsView` | SimpleX Lock, delivery receipts, link previews, auto-accept | +| Appearance | `ic_light_mode` | `AppearanceView` | Theme, language, profile images, chat bubbles | + +All rows disabled when `chatModel.chatRunning != true` (except Appearance). + +#### Notifications (`NotificationsSettingsView`) + +| Setting | Options | +|---|---| +| Notification mode | Instant (background service) / Periodic (every 10 min) / Off | +| Notification preview | Configuration for notification content visibility | + +#### Network & Servers (`NetworkAndServersView`) + +| Setting | Description | +|---|---| +| SMP servers | Messaging relay servers; per-operator configuration | +| XFTP servers | File transfer servers; per-operator configuration | +| Server operators | `OperatorView` for each configured operator | +| Advanced network | `AdvancedNetworkSettings` -- timeouts, TCP keep-alive, reconnect intervals | +| Proxy configuration | SOCKS proxy, .onion host settings | + +Sub-files: `NetworkAndServers.kt`, `ProtocolServersView.kt`, `ProtocolServerView.kt`, `NewServerView.kt`, `ScanProtocolServer.kt`, `AdvancedNetworkSettings.kt`, `OperatorView.kt` + +#### Audio & Video Calls (`CallSettingsView`) + +| Setting | Description | +|---|---| +| WebRTC relay policy | Always relay / relay when needed / never relay | +| ICE servers | Custom STUN/TURN server configuration | + +#### Privacy & Security (`PrivacySettingsView`) + +Organized in sections: + +**Device Section** (`PrivacyDeviceSection`): + +| Setting | Description | +|---|---| +| SimpleX Lock | `SimplexLockView` -- app lock with system auth or passcode (`LAMode.SYSTEM` / `LAMode.PASSCODE`) | + +**Chats Section**: + +| Setting | Preference Key | Description | +|---|---|---| +| Send link previews | `privacyLinkPreviews` | Auto-generate link preview cards | +| Sanitize links | `privacySanitizeLinks` | Strip tracking parameters from URLs | +| Show last messages | `privacyShowChatPreviews` | Show message previews in chat list | +| Message draft | `privacySaveLastDraft` | Save unsent message draft for each chat | + +**Files Section**: + +| Setting | Preference Key | Description | +|---|---|---| +| Encrypt local files | `privacyEncryptLocalFiles` | Encrypt files stored on device | +| Auto-accept images | `privacyAcceptImages` | Automatically download received images | +| Blur media radius | `privacyMediaBlurRadius` | Blur radius for media previews | +| Protect IP address | `privacyAskToApproveRelays` | Prompt before connecting to unknown file relays to protect IP address | + +#### Appearance (`AppearanceView`) + +Platform-specific composable (`expect fun AppearanceView`): + +| Setting | Description | +|---|---| +| Profile images | `ProfileImageSection` -- slider for profile image corner radius | +| Theme selection | Color scheme / theme picker | +| Language | App language selection | +| Chat wallpaper | Background image settings | +| Chat bubbles | Message bubble appearance configuration | +| Toolbar opacity | App bar transparency settings (`inAppBarsAlpha`) | +| Color picker | `ClassicColorPicker` for custom theme colors | + +### Chat Database Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| Database passphrase & export | `ic_database` | `DatabaseView` | Manage encryption, export/import database | +| Migrate to another device | `ic_ios_share` | `MigrateFromDeviceView` | Device migration (requires auth) | + +Database icon shows warning color (`WarningOrange`) when database is not encrypted or passphrase is not saved. + +### Help Section + +| Row | Icon | Destination | Description | +|---|---|---|---| +| How to use SimpleX Chat | `ic_help` | `HelpView` | Usage guide | +| What's new | `ic_add` | `WhatsNewView` | Version changelog | +| About SimpleX Chat | `ic_info` | `SimpleXInfo` (non-onboarding mode) | App information | +| Chat with the founder | `ic_tag` | Opens SimpleX link | Direct chat with SimpleX team | +| Send us an email | `ic_mail` | Opens mailto: | Email support | + +### Support Section + +| Row | Icon | Description | +|---|---|---| +| Contribute | `ic_keyboard` | Opens GitHub contribution page (hidden for Android Bundle) | +| Rate the app | `ic_star` | Opens Google Play / app store listing | +| Star on GitHub | `ic_github` | Opens GitHub repository | + +### App Section (`SettingsSectionApp`) + +Platform-specific section (expect/actual composable): + +| Row | Description | +|---|---| +| App updates (Desktop) | App update checker and installer | +| Developer tools | Toggle developer mode | +| Chat console | Opens `ChatConsoleView` terminal | +| Terminal always visible (Desktop) | Keep terminal window open | +| Install terminal app | Link to CLI app on GitHub | +| Reset all hints | Reset dismissed hint/card preferences | +| App version | Version string with build info; taps open `VersionInfoView` | + +## Source Files + +| File | Path | +|---|---| +| `SettingsView.kt` | `views/usersettings/SettingsView.kt` | +| `Appearance.kt` | `views/usersettings/Appearance.kt` | +| `PrivacySettings.kt` | `views/usersettings/PrivacySettings.kt` | +| `NetworkAndServers.kt` | `views/usersettings/networkAndServers/NetworkAndServers.kt` | +| `AdvancedNetworkSettings.kt` | `views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | +| `OperatorView.kt` | `views/usersettings/networkAndServers/OperatorView.kt` | +| `ProtocolServersView.kt` | `views/usersettings/networkAndServers/ProtocolServersView.kt` | +| `NewServerView.kt` | `views/usersettings/networkAndServers/NewServerView.kt` | diff --git a/apps/multiplatform/product/views/user-profiles.md b/apps/multiplatform/product/views/user-profiles.md new file mode 100644 index 0000000000..dfc37a5e8d --- /dev/null +++ b/apps/multiplatform/product/views/user-profiles.md @@ -0,0 +1,122 @@ +# User Profiles + +> **Related spec:** [spec/client/navigation.md](../../spec/client/navigation.md) + +## Purpose + +Manage multiple chat profiles within a single app instance. Users can create, switch between, hide, mute, and delete profiles. Hidden profiles are protected by password. The UserPicker provides quick profile switching from the chat list, while UserProfilesView offers full profile management. + +## Route / Navigation + +- **Entry point**: Tap user avatar in `ChatListView` toolbar -> `UserPicker` -> "Your chat profiles" +- **Presented by**: `UserProfilesView` composable via `ModalManager.start.showCustomModal` with search bar +- **Navigation title**: "Your chat profiles" (`AppBarTitle`) +- **Sub-navigation**: + - Create profile -> `CreateProfile` (via `ModalManager.center`) + - Edit active profile -> `UserProfileView` (via UserPicker tap on active user) + - User address -> `UserAddressView` (via UserPicker) + - Chat preferences -> `PreferencesView` (via UserPicker) + +## Page Sections + +### UserPicker (`UserPicker.kt`) + +Overlay panel triggered from `ChatListView` toolbar: + +| Section | Description | +|---|---| +| Device picker row | `DevicePickerRow` showing local device and connected remote hosts (Desktop only); pill-shaped buttons with connect/disconnect actions | +| Active user profile | `ProfilePreview` of current user (Desktop: single row; Android: full user list) | +| User list | `UserPickerUsersSection` with all visible non-hidden profiles; tap to switch, long-press disabled | +| SimpleX address | Row to open `UserAddressView` (create or view address) | +| Chat preferences | Row to open `PreferencesView` | +| Chat profiles | Row to open `UserProfilesView` (or `CreateProfile` when no users exist on Desktop) | +| Use from desktop/mobile | Android: "Use from desktop" (`ConnectDesktopView`); Desktop: "Link a mobile" / "Linked mobiles" (`ConnectMobileView`) | +| Settings | Row to open `SettingsView` with `ColorModeSwitcher` trailing | + +Platform behavior: +- **Android**: `PlatformUserPicker` renders as bottom sheet with `AnimatedViewState` transitions; shows all users inline +- **Desktop**: Sidebar panel; shows only active user in header, inactive users in separate section below divider + +### UserProfilesView + +Full profile management screen with search/password field: + +#### Search / Password Field + +Combined text field at the top (`searchTextOrPassword`): +- In normal mode: Filters visible profiles by name +- For hidden profiles: Acts as password entry to reveal hidden profiles +- Trimmed search text compared against `user.anyNameContains()` and `correctPassword()` + +#### Profile List + +Each row rendered by `UserView` -> `UserProfilePickerItem`: + +| Element | Description | +|---|---| +| Active indicator | Checkmark icon (`ic_done_filled`) for the current active profile | +| Profile image | 54dp avatar with `fontSizeSqrtMultiplier` scaling | +| Display name | Profile's display name; bold for active, normal for inactive | +| Unread count | Badge showing unread message count (`unreadCountStr`) with primary/secondary color based on mute state | +| Muted indicator | `ic_notifications_off` icon when profile notifications are muted | +| Hidden indicator | `ic_lock` icon for hidden profiles (only shown when revealed via password) | + +#### Profile Row Tap Action + +| Action | Description | +|---|---| +| Switch active | Tapping a profile row calls `changeActiveUser()` to activate the selected profile; all chats switch context | + +#### Profile Actions (Context Menu) + +Available via long-press / right-click on a profile row (`DefaultDropdownMenu`): + +| Action | Condition | Description | +|---|---|---| +| Mute | Visible, notifications on | `apiMuteUser()` mutes notifications; shows `showMuteProfileAlert` on first use | +| Unmute | Visible, notifications off | `apiUnmuteUser()` restores notifications | +| Hide | Visible, multiple visible users | Opens `HiddenProfileView` to set password | +| Unhide | Hidden profile | `apiUnhideUser()` with password entry (`ProfileActionView` with `UserProfileAction.UNHIDE`) | +| Delete | Any non-sole profile | Delete with confirmation dialog; options: "Delete with connections" (removes SMP queues) or "Delete data only" | + +#### Add Profile + +| Element | Description | +|---|---| +| Add button | "+" icon with "Add profile" text at bottom of list (hidden when searching) | +| Auth required | Profile creation requires authentication via `withAuth` | +| Create view | Opens `CreateProfile` in `ModalManager.center` | + +#### Profile Deletion (`removeUser`) + +Deletion flow: +1. If hidden profile requiring password: opens `ProfileActionView` with `UserProfileAction.DELETE` +2. If active profile: switches to another visible user first via `changeActiveUser_`, then deletes +3. If last visible profile with hidden profiles: deletes user, then changes active to null; on Android, stops chat and resets to onboarding +4. Cleans up wallpaper files and cancels notifications for the deleted user + +#### Hidden Profile Notice + +Shown once via `showHiddenProfilesNotice` preference: + +| Element | Description | +|---|---| +| Alert title | "Make profile private" | +| Alert text | "You can hide or mute user profile" | +| "Don't show again" | Disables the notice permanently | + +### Profile Password Validation + +| Function | Description | +|---|---| +| `correctPassword()` | Validates password against `user.viewPwdHash` using `chatPasswordHash(pwd, salt)` | +| `passwordEntryRequired()` | Returns true if user is hidden, active, and password does not match current search text | +| `userViewPassword()` | Extracts view password from search text for hidden user operations | + +## Source Files + +| File | Path | +|---|---| +| `UserProfilesView.kt` | `views/usersettings/UserProfilesView.kt` | +| `UserPicker.kt` | `views/chatlist/UserPicker.kt` | diff --git a/apps/multiplatform/spec/README.md b/apps/multiplatform/spec/README.md new file mode 100644 index 0000000000..c5d9a3b4f7 --- /dev/null +++ b/apps/multiplatform/spec/README.md @@ -0,0 +1,137 @@ +# SimpleX Chat -- Kotlin Multiplatform Specification + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Dependency Graph](#dependency-graph) +3. [Specification Documents](#specification-documents) +4. [Product Documents](#product-documents) +5. [Source Entry Points](#source-entry-points) + +--- + +## Executive Summary + +SimpleX Chat is a Kotlin Multiplatform application targeting **Android** and **Desktop** (JVM) platforms. The UI layer is built entirely with Jetpack Compose. The application communicates with a Haskell-based cryptographic core (`simplex-chat`) through a **JNI bridge** -- native functions declared in Kotlin and linked at runtime to a shared library (`libapp-lib`). Platform-specific behavior (notifications, file system paths, services, audio/video) is abstracted using the `expect`/`actual` pattern and a runtime-assignable `PlatformInterface` callback object. + +The Gradle project is structured as three modules: + +| Module | Purpose | +|---|---| +| `:common` | Shared Compose UI, models, platform abstractions (`commonMain`, `androidMain`, `desktopMain`) | +| `:android` | Android application entry point (`SimplexApp`, `MainActivity`) | +| `:desktop` | Desktop application entry point (`Main.kt`, `showApp()`) | + +All meaningful application logic resides in `:common/commonMain`. Platform source sets (`androidMain`, `desktopMain`) provide `actual` implementations for `expect` declarations and host platform-specific integration code. + +--- + +## Dependency Graph + +``` +App Entry Points ++-- Android: SimplexApp.onCreate -> initHaskell -> initMultiplatform -> initChatControllerOnStart +| MainActivity.onCreate -> setContent { AppScreen() } ++-- Desktop: main() -> initHaskell -> runMigrations -> initApp -> showApp -> AppWindow -> AppScreen() + | + v +Common Module (commonMain) ++-- ChatModel (Compose state singleton) <-> ChatController/SimpleXAPI (JNI bridge) <-> Haskell Core (chat_ctrl) ++-- Views (Compose) +| +-- App.kt: AppScreen -> MainScreen +| +-- ChatListView -> ChatView -> ComposeView -> SendMsgView +| +-- ChatItemView (message rendering: text, image, video, voice, file, call, events) +| +-- Settings: SettingsView, UserProfileView, UserProfilesView +| +-- Onboarding: OnboardingView, WhatsNewView, CreateFirstProfile +| +-- Call: CallView, IncomingCallAlertView +| +-- Database: DatabaseView, DatabaseEncryptionView, DatabaseErrorView +| +-- Groups: GroupChatInfoView, AddGroupMembersView, GroupMemberInfoView +| +-- Contacts: ContactListNavView +| +-- Remote: ConnectDesktopView, ConnectMobileView +| +-- Terminal: TerminalView ++-- Models +| +-- ChatModel -- global app state (Compose MutableState singleton) +| +-- ChatsContext -- per-context chat list state (primary + optional secondary) +| +-- Chat -- per-conversation state (chatInfo, chatItems, chatStats) +| +-- ChatController -- API command dispatch, event receiver, preferences +| +-- AppPreferences -- 150+ SharedPreferences keys ++-- Services +| +-- NtfManager -- abstract notification coordinator (Android/Desktop implementations) +| +-- SimplexService -- Android foreground service for background messaging +| +-- ThemeManager -- theme resolution (system/light/dark/simplex/black + per-user overrides) +| +-- CallManager -- WebRTC call lifecycle ++-- Platform (expect/actual) + +-- Core.kt -- JNI declarations (external fun), initChatController, chatInitTemporaryDatabase + +-- AppCommon.kt -- runMigrations, AppPlatform enum + +-- Files.kt -- dataDir, tmpDir, filesDir, dbAbsolutePrefixPath (expect) + +-- Share.kt -- shareText, shareFile, openFile (expect) + +-- VideoPlayer.kt -- VideoPlayerInterface, VideoPlayer (expect class) + +-- RecAndPlay.kt -- RecorderInterface, AudioPlayerInterface (expect) + +-- UI.kt -- showToast, hideKeyboard, getKeyboardState (expect) + +-- Notifications.kt -- allowedToShowNotification (expect) + +-- NtfManager.kt -- abstract NtfManager class + +-- Platform.kt -- PlatformInterface (runtime callback object) + +-- Cryptor.kt -- CryptorInterface (expect) + +-- Images.kt -- bitmap utilities (expect) + +-- SimplexService.kt-- getWakeLock (expect) + +-- Log.kt, Modifier.kt, Back.kt, ScrollableColumn.kt, PlatformTextField.kt, Resources.kt +``` + +--- + +## Specification Documents + +| Document | Path | Description | +|---|---|---| +| Architecture | [spec/architecture.md](architecture.md) | System layers, module structure, JNI bridge, app lifecycle, event streaming, platform abstraction | +| State Management | [spec/state.md](state.md) | ChatModel singleton, ChatsContext, Chat data class, AppPreferences, ActiveChatState | +| API | [spec/api.md](api.md) | ChatController command dispatch, ~150 API functions in 11 categories, CC/CR/API types | +| Database | [spec/database.md](database.md) | SQLite database files, migrations, encryption, backup/restore | +| Impact | [spec/impact.md](impact.md) | Source file → product concept mapping for change impact analysis | +| Chat View | [spec/client/chat-view.md](client/chat-view.md) | ChatView, ChatItemView, message rendering, item interactions | +| Chat List | [spec/client/chat-list.md](client/chat-list.md) | ChatListView, ChatPreviewView, filtering, search, tags | +| Compose | [spec/client/compose.md](client/compose.md) | ComposeView, SendMsgView, ComposeState, attachments, mentions | +| Navigation | [spec/client/navigation.md](client/navigation.md) | App screen routing, onboarding, settings, new chat flows | +| Calls | [spec/services/calls.md](services/calls.md) | WebRTC call lifecycle, signaling, platform-specific call views | +| Files | [spec/services/files.md](services/files.md) | File transfer (SMP inline / XFTP), CryptoFile encryption, platform file paths | +| Notifications | [spec/services/notifications.md](services/notifications.md) | NtfManager, SimplexService, notification channels, background delivery | +| Theme | [spec/services/theme.md](services/theme.md) | ThemeManager, color system, wallpapers, per-user overrides | + +--- + +## Product Documents + +| Category | Path | Topic | +|---|---|---| +| Overview | [product/README.md](../product/README.md) | Product overview, capability map, navigation map | +| Concepts | [product/concepts.md](../product/concepts.md) | 30 product concepts (PC1-PC30) mapped to docs + source | +| Glossary | [product/glossary.md](../product/glossary.md) | Domain term definitions (9 sections) | +| Rules | [product/rules.md](../product/rules.md) | 18 business rules in 6 categories | +| Gaps | [product/gaps.md](../product/gaps.md) | 7 known gaps with recommendations | +| Flows | [product/flows/](../product/flows/) | onboarding, messaging, connection, calling, file-transfer, group-lifecycle | +| Views | [product/views/](../product/views/) | chat-list, chat, settings, onboarding, call, new-chat, contact-info, group-info, user-profiles | + +--- + +## Source Entry Points + +| Component | File | Key Symbol | Line | +|---|---|---|---| +| Android Application | [`SimplexApp.kt`](../android/src/main/java/chat/simplex/app/SimplexApp.kt#L41) | `class SimplexApp` | 41 | +| Android Activity | [`MainActivity.kt`](../android/src/main/java/chat/simplex/app/MainActivity.kt#L27) | `class MainActivity` | 27 | +| Desktop Entry | [`Main.kt`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt#L21) | `fun main()` | 21 | +| Desktop App Window | [`DesktopApp.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt#L33) | `fun showApp()` | 33 | +| Desktop Init | [`AppCommon.desktop.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt#L21) | `fun initApp()` | 21 | +| Common App Screen | [`App.kt`](../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L47) | `fun AppScreen()` | 47 | +| JNI Bridge | [`Core.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L18) | `external fun initHS()` | 18 | +| Chat Controller | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L493) | `object ChatController` | 493 | +| Chat Model | [`ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L86) | `object ChatModel` | 86 | +| App Preferences | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L94) | `class AppPreferences` | 94 | +| Platform Interface | [`Platform.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L15) | `interface PlatformInterface` | 15 | +| Notification Manager | [`NtfManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L19) | `abstract class NtfManager` | 19 | +| Theme Manager | [`ThemeManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L18) | `object ThemeManager` | 18 | +| Android Haskell Init | [`AppCommon.android.kt`](../common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt#L33) | `fun initHaskell(packageName: String)` | 33 | +| Common Migrations | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L41) | `fun runMigrations()` | 41 | +| Android Service | [`SimplexService.kt`](../android/src/main/java/chat/simplex/app/SimplexService.kt#L41) | `class SimplexService` | 41 | +| Gradle Root | [`settings.gradle.kts`](../settings.gradle.kts#L22) | `include(":android", ":desktop", ":common")` | 22 | +| Common Build | [`build.gradle.kts`](../common/build.gradle.kts#L14) | `kotlin { androidTarget(); jvm("desktop") }` | 14 | diff --git a/apps/multiplatform/spec/api.md b/apps/multiplatform/spec/api.md new file mode 100644 index 0000000000..15d5e141a0 --- /dev/null +++ b/apps/multiplatform/spec/api.md @@ -0,0 +1,435 @@ +# Chat API Reference + +## Table of Contents + +1. [Overview](#1-overview) +2. [Command Categories](#2-command-categories) + - 2.1 [User Management](#21-user-management) + - 2.2 [Chat Lifecycle](#22-chat-lifecycle) + - 2.3 [Message Operations](#23-message-operations) + - 2.4 [Group Operations](#24-group-operations) + - 2.5 [Contact Operations](#25-contact-operations) + - 2.6 [File Operations](#26-file-operations) + - 2.7 [Call Operations](#27-call-operations) + - 2.8 [Settings & Network](#28-settings--network) + - 2.9 [Chat Tags](#29-chat-tags) + - 2.10 [Server Operators](#210-server-operators) + - 2.11 [Archive](#211-archive) +3. [Response Types](#3-response-types) +4. [Event Types](#4-event-types) +5. [Error Types](#5-error-types) +6. [Source Files](#6-source-files) + +--- + +## 1. Overview + +The SimpleX Chat API bridge connects Kotlin/Compose UI code to the Haskell core via JNI. All communication follows a **command/response JSON protocol**: + +``` +Kotlin suspend fun api*() + -> ChatController.sendCmd(rhId, CC.*, ctrl) + -> serialize CC to cmdString (JSON) + -> chatSendCmdRetry(ctrl, cmdString, retryNum) [JNI / external fun] + -> Haskell core processes command + -> returns JSON response string + -> json.decodeFromString(responseString) + -> API.Result(rhId, CR.*) or API.Error(rhId, ChatError) + -> pattern-match on CR subclass -> update ChatModel / return data to UI +``` + +**Key types in the pipeline:** + +| Type | Role | Location | +|------|------|----------| +| `CC` (sealed class) | Command definitions (~165 subclasses) | [SimpleXAPI.kt#L3529](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L3529) | +| `API` (sealed class) | Top-level response wrapper (`Result` / `Error`) | [SimpleXAPI.kt#L5975](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L5975) | +| `CR` (sealed class) | Chat response variants (~180 subclasses) | [SimpleXAPI.kt#L6114](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6114) | +| `ChatError` (sealed class) | Error hierarchy | [SimpleXAPI.kt#L6974](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6974) | +| `ChatController` (object) | Singleton hosting all `api*` functions | [SimpleXAPI.kt#L493](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L493) | + +**JNI bridge functions** (declared in [Core.kt#L25](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L25)): + +```kotlin +external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array +external fun chatCloseStore(ctrl: ChatCtrl): String +external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String +external fun chatSendRemoteCmdRetry(ctrl: ChatCtrl, rhId: Int, msg: String, retryNum: Int): String +external fun chatRecvMsg(ctrl: ChatCtrl): String +external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String +``` + + + +**`sendCmd` flow** ([SimpleXAPI.kt#L804](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L804)): + +1. Obtains the `ChatCtrl` handle (or uses the provided `otherCtrl`). +2. Serializes the `CC` command to its `cmdString`. +3. Dispatches to `Dispatchers.IO`; calls `chatSendCmdRetry` (local) or `chatSendRemoteCmdRetry` (remote host). +4. Decodes the returned JSON string into `API`. +5. Logs the result to the terminal item list. + + + + + +**Asynchronous event receiver** (`startReceiver`, [SimpleXAPI.kt#L660](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L660)): + +A long-running coroutine on `Dispatchers.IO` repeatedly calls `chatRecvMsgWait` (blocking JNI). Each received `API` message is dispatched to `processReceivedMsg` ([SimpleXAPI.kt#L2568](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2568)), which pattern-matches on `CR` subclasses to update `ChatModel` state and trigger notifications. + +--- + + + +## 2. Command Categories + +All functions below are `suspend fun` members of `ChatController` ([SimpleXAPI.kt#L493](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L493)). The `rh` / `rhId` parameter is `Long?` identifying a remote host (`null` = local device). + +### 2.1 User Management + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiGetActiveUser` | `rh: Long?, ctrl: ChatCtrl?` | Fetch the currently active user profile | [L841](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L841) | +| `apiCreateActiveUser` | `rh: Long?, p: Profile?, pastTimestamp: Boolean, ctrl: ChatCtrl?` | Create a new user profile and set it as active | [L851](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L851) | +| `listUsers` | `rh: Long?` | List all user profiles sorted by display name | [L871](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L871) | +| `apiSetActiveUser` | `rh: Long?, userId: Long, viewPwd: String?` | Switch the active user to a different profile | [L881](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L881) | +| `apiSetAllContactReceipts` | `rh: Long?, enable: Boolean` | Enable/disable delivery receipts for all contacts globally | [L888](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L888) | +| `apiSetUserContactReceipts` | `u: User, userMsgReceiptSettings: UserMsgReceiptSettings` | Set delivery receipt settings for user contacts | [L894](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L894) | +| `apiSetUserGroupReceipts` | `u: User, userMsgReceiptSettings: UserMsgReceiptSettings` | Set delivery receipt settings for user groups | [L900](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L900) | +| `apiSetUserAutoAcceptMemberContacts` | `u: User, enable: Boolean` | Toggle auto-accept for member contact requests | [L906](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L906) | +| `apiHideUser` | `u: User, viewPwd: String` | Hide a user profile behind a password | [L912](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L912) | +| `apiUnhideUser` | `u: User, viewPwd: String` | Unhide a previously hidden user profile | [L915](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L915) | +| `apiMuteUser` | `u: User` | Mute all notifications for a user profile | [L918](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L918) | +| `apiUnmuteUser` | `u: User` | Unmute notifications for a user profile | [L921](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L921) | +| `apiDeleteUser` | `u: User, delSMPQueues: Boolean, viewPwd: String?` | Delete a user profile and optionally its SMP queues | [L930](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L930) | +| `apiUpdateProfile` | `rh: Long?, profile: Profile` | Update the active user's display profile | [L1682](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1682) | +| `apiSetProfileAddress` | `rh: Long?, on: Boolean` | Enable/disable including address in user profile | [L1694](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1694) | +| `apiSetUserUIThemes` | `rh: Long?, userId: Long, themes: ThemeModeOverrides?` | Set UI theme overrides for a user | [L1732](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1732) | + +### 2.2 Chat Lifecycle + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiStartChat` | `ctrl: ChatCtrl?` | Start the chat engine (returns `true` if newly started) | [L937](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L937) | +| `apiStopChat` | _(none)_ | Stop the chat engine | [L955](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L955) | +| `apiSetAppFilePaths` | `filesFolder, tempFolder, assetsFolder, remoteHostsFolder: String, ctrl: ChatCtrl?` | Configure file-system paths for the Haskell core | [L961](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L961) | +| `apiSetEncryptLocalFiles` | `enable: Boolean` | Enable/disable encryption of locally stored files | [L967](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L967) | +| `apiSaveAppSettings` | `settings: AppSettings` | Persist application settings to the core | [L969](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L969) | +| `apiGetAppSettings` | `settings: AppSettings` | Retrieve application settings from the core | [L975](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L975) | +| `apiGetChats` | `rh: Long?` | Fetch the list of all chats for the active user | [L1013](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1013) | +| `apiGetChat` | `rh, type, id, scope, contentTag, pagination, search` | Fetch a single chat with paginated messages | [L1031](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1031) | +| `apiGetChatContentTypes` | `rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?` | Get available content type filters for a chat | [L1044](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1044) | +| `apiClearChat` | `rh: Long?, type: ChatType, id: Long` | Delete all messages in a chat | [L1675](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1675) | +| `apiDeleteChat` | `rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode` | Delete a chat (contact, group, connection, etc.) | [L1620](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1620) | +| `apiChatRead` | `rh: Long?, type: ChatType, id: Long` | Mark a chat as read | [L1888](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1888) | +| `apiChatItemsRead` | `rh, type, id, scope, itemIds` | Mark specific chat items as read | [L1902](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1902) | +| `apiChatUnread` | `rh: Long?, type: ChatType, id: Long, unreadChat: Boolean` | Toggle a chat's unread flag | [L1909](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1909) | +| `getChatItemTTL` | `rh: Long?` | Get the auto-delete TTL for chat items | [L1286](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1286) | +| `setChatItemTTL` | `rh: Long?, chatItemTTL: ChatItemTTL` | Set the auto-delete TTL for chat items | [L1299](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1299) | +| `setChatTTL` | `rh: Long?, chatType, id, chatItemTTL` | Set TTL for a specific chat | [L1306](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1306) | + +### 2.3 Message Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiSendMessages` | `rh, type, id, scope, live, ttl, composedMessages` | Send one or more messages to a chat | [L1074](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1074) | +| `apiCreateChatItems` | `rh: Long?, noteFolderId: Long, composedMessages: List` | Create items in a private notes folder | [L1111](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1111) | +| `apiReportMessage` | `rh, groupId, chatItemId, reportReason, reportText` | Report a message in a group | [L1119](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1119) | +| `apiGetChatItemInfo` | `rh, type, id, scope, itemId` | Get delivery info for a specific chat item | [L1126](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1126) | +| `apiForwardChatItems` | `rh, toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl` | Forward messages between chats | [L1133](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1133) | +| `apiPlanForwardChatItems` | `rh, fromChatType, fromChatId, fromScope, chatItemIds` | Check forward feasibility before forwarding | [L1138](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1138) | +| `apiUpdateChatItem` | `rh, type, id, scope, itemId, updatedMessage, live` | Edit an existing message | [L1145](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1145) | +| `apiChatItemReaction` | `rh, type, id, scope, itemId, add, reaction` | Add or remove a reaction to a message | [L1168](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1168) | +| `apiGetReactionMembers` | `rh: Long?, groupId: Long, itemId: Long, reaction: MsgReaction` | List members who reacted with a specific emoji | [L1175](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1175) | +| `apiDeleteChatItems` | `rh, type, id, scope, itemIds, mode` | Delete messages (for self or for everyone) | [L1183](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1183) | +| `apiDeleteMemberChatItems` | `rh: Long?, groupId: Long, itemIds: List` | Moderate: delete another member's messages | [L1190](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1190) | +| `apiArchiveReceivedReports` | `rh: Long?, groupId: Long` | Archive all received reports in a group | [L1197](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1197) | +| `apiDeleteReceivedReports` | `rh: Long?, groupId: Long, itemIds: List, mode: CIDeleteMode` | Delete specific received reports | [L1204](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1204) | + +### 2.4 Group Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiNewGroup` | `rh: Long?, incognito: Boolean, groupProfile: GroupProfile` | Create a new group | [L2092](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2092) | +| `apiAddMember` | `rh: Long?, groupId: Long, contactId: Long, memberRole: GroupMemberRole` | Invite a contact to a group | [L2100](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2100) | +| `apiJoinGroup` | `rh: Long?, groupId: Long` | Accept a group invitation | [L2109](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2109) | +| `apiAcceptMember` | `rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole` | Accept a member joining via group link | [L2135](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2135) | +| `apiDeleteMemberSupportChat` | `rh: Long?, groupId: Long, groupMemberId: Long` | Delete a member's support chat | [L2144](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2144) | +| `apiRemoveMembers` | `rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean` | Remove members from a group | [L2151](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2151) | +| `apiMembersRole` | `rh: Long?, groupId: Long, memberIds: List, memberRole: GroupMemberRole` | Change the role of group members | [L2160](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2160) | +| `apiBlockMembersForAll` | `rh: Long?, groupId: Long, memberIds: List, blocked: Boolean` | Block/unblock members for all group participants | [L2169](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2169) | +| `apiLeaveGroup` | `rh: Long?, groupId: Long` | Leave a group | [L2178](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2178) | +| `apiListMembers` | `rh: Long?, groupId: Long` | List all members of a group | [L2185](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2185) | +| `apiUpdateGroup` | `rh: Long?, groupId: Long, groupProfile: GroupProfile` | Update group profile (name, image, etc.) | [L2192](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2192) | +| `apiCreateGroupLink` | `rh: Long?, groupId: Long, memberRole: GroupMemberRole` | Create a group invitation link | [L2211](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2211) | +| `apiGroupLinkMemberRole` | `rh: Long?, groupId: Long, memberRole: GroupMemberRole` | Update the default role for group link joins | [L2226](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2226) | +| `apiDeleteGroupLink` | `rh: Long?, groupId: Long` | Delete the group invitation link | [L2235](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2235) | +| `apiGetGroupLink` | `rh: Long?, groupId: Long` | Retrieve the current group invitation link | [L2245](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2245) | +| `apiAddGroupShortLink` | `rh: Long?, groupId: Long` | Create a short link for the group | [L2252](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2252) | +| `apiCreateMemberContact` | `rh: Long?, groupId: Long, groupMemberId: Long` | Create a direct contact from a group member | [L2262](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2262) | +| `apiSendMemberContactInvitation` | `rh: Long?, contactId: Long, mc: MsgContent` | Send a direct message invitation to a group member | [L2271](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2271) | +| `apiAcceptMemberContact` | `rh: Long?, contactId: Long` | Accept a member's direct contact invitation | [L2280](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2280) | +| `apiSetMemberSettings` | `rh: Long?, groupId: Long, groupMemberId: Long, memberSettings: GroupMemberSettings` | Configure per-member settings (e.g., mentions) | [L1343](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1343) | +| `apiGroupMemberInfo` | `rh: Long?, groupId: Long, groupMemberId: Long` | Get a group member's info and connection stats | [L1353](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1353) | +| `apiSetGroupAlias` | `rh: Long?, groupId: Long, localAlias: String` | Set a local alias for a group | [L1718](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1718) | + +### 2.5 Contact Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiAddContact` | `rh: Long?, incognito: Boolean` | Create a one-time invitation link for a new contact | [L1444](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1444) | +| `apiSetConnectionIncognito` | `rh: Long?, connId: Long, incognito: Boolean` | Toggle incognito on a pending connection | [L1455](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1455) | +| `apiChangeConnectionUser` | `rh: Long?, connId: Long, userId: Long` | Change the user profile on a pending connection | [L1464](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1464) | +| `apiConnectPlan` | `rh: Long?, connLink: String, inProgress: MutableState` | Analyze a connection link before connecting | [L1474](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1474) | +| `apiConnect` | `rh: Long?, incognito: Boolean, connLink: CreatedConnLink` | Connect via an invitation or address link | [L1482](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1482) | +| `apiPrepareContact` | `rh, connLink, contactShortLinkData` | Prepare a contact chat from a short link (before connecting) | [L1546](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1546) | +| `apiPrepareGroup` | `rh, connLink, groupShortLinkData` | Prepare a group chat from a short link (before connecting) | [L1555](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1555) | +| `apiConnectPreparedContact` | `rh, contactId, incognito, msg` | Connect to a previously prepared contact | [L1580](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1580) | +| `apiConnectPreparedGroup` | `rh, groupId, incognito, msg` | Join a previously prepared group | [L1590](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1590) | +| `apiConnectContactViaAddress` | `rh: Long?, incognito: Boolean, contactId: Long` | Connect to a contact using their public address | [L1600](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1600) | +| `apiDeleteContact` | `rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode` | Delete a contact and return the deleted Contact | [L1644](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1644) | +| `apiContactInfo` | `rh: Long?, contactId: Long` | Get a contact's connection stats and custom profile | [L1346](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1346) | +| `apiSetContactAlias` | `rh: Long?, contactId: Long, localAlias: String` | Set a local display alias for a contact | [L1711](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1711) | +| `apiSetConnectionAlias` | `rh: Long?, connId: Long, localAlias: String` | Set a local display alias for a pending connection | [L1725](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1725) | +| `apiSetContactPrefs` | `rh: Long?, contactId: Long, prefs: ChatPreferences` | Update feature preferences for a contact | [L1704](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1704) | +| `apiCreateUserAddress` | `rh: Long?` | Create a long-term public contact address | [L1746](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1746) | +| `apiDeleteUserAddress` | `rh: Long?` | Delete the user's public contact address | [L1762](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1762) | +| `apiAddMyAddressShortLink` | `rh: Long?` | Create a short link for the user's address | [L1784](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1784) | +| `apiSetUserAddressSettings` | `rh: Long?, settings: AddressSettings` | Configure auto-accept for incoming contact requests | [L1795](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1795) | +| `apiAcceptContactRequest` | `rh: Long?, incognito: Boolean, contactReqId: Long` | Accept an incoming contact request | [L1809](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1809) | +| `apiRejectContactRequest` | `rh: Long?, contactReqId: Long` | Reject an incoming contact request | [L1832](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1832) | +| `apiSwitchContact` | `rh: Long?, contactId: Long` | Initiate SMP server switch for a contact | [L1374](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1374) | +| `apiAbortSwitchContact` | `rh: Long?, contactId: Long` | Abort an in-progress server switch | [L1388](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1388) | +| `apiSyncContactRatchet` | `rh: Long?, contactId: Long, force: Boolean` | Force ratchet synchronization with a contact | [L1402](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1402) | +| `apiGetContactCode` | `rh: Long?, contactId: Long` | Get the security verification code for a contact | [L1416](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1416) | +| `apiVerifyContact` | `rh: Long?, contactId: Long, connectionCode: String?` | Verify a contact's security code | [L1430](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1430) | + +### 2.6 File Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `receiveFiles` | `rhId, user, fileIds, userApprovedRelays, auto` | Accept and download one or more files (handles relay approval) | [L1946](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1946) | +| `receiveFile` | `rhId, user, fileId, userApprovedRelays, auto` | Accept and download a single file (convenience wrapper) | [L2062](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2062) | +| `cancelFile` | `rh: Long?, user: User, fileId: Long` | Cancel an in-progress file transfer and clean up | [L2072](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2072) | +| `apiCancelFile` | `rh: Long?, fileId: Long, ctrl: ChatCtrl?` | Cancel a file transfer (low-level, returns updated chat item) | [L2080](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2080) | +| `uploadStandaloneFile` | `user: UserLike, file: CryptoFile, ctrl: ChatCtrl?` | Upload a standalone file (used for migration) | [L1916](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1916) | +| `downloadStandaloneFile` | `user: UserLike, url: String, file: CryptoFile, ctrl: ChatCtrl?` | Download a standalone file by URL | [L1926](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1926) | +| `standaloneFileInfo` | `url: String, ctrl: ChatCtrl?` | Retrieve metadata for a standalone file link | [L1936](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1936) | + +### 2.7 Call Operations + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiGetCallInvitations` | `rh: Long?` | Retrieve pending call invitations | [L1842](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1842) | +| `apiSendCallInvitation` | `rh: Long?, contact: Contact, callType: CallType` | Initiate a call by sending an invitation | [L1849](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1849) | +| `apiRejectCall` | `rh: Long?, contact: Contact` | Reject an incoming call | [L1854](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1854) | +| `apiSendCallOffer` | `rh, contact, rtcSession, rtcIceCandidates, media, capabilities` | Send a WebRTC call offer | [L1859](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1859) | +| `apiSendCallAnswer` | `rh: Long?, contact: Contact, rtcSession: String, rtcIceCandidates: String` | Send a WebRTC call answer | [L1866](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1866) | +| `apiSendCallExtraInfo` | `rh: Long?, contact: Contact, rtcIceCandidates: String` | Send additional ICE candidates during a call | [L1872](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1872) | +| `apiEndCall` | `rh: Long?, contact: Contact` | End an active call | [L1878](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1878) | +| `apiCallStatus` | `rh: Long?, contact: Contact, status: WebRTCCallStatus` | Report call status updates to the core | [L1883](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1883) | + +### 2.8 Settings & Network + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiSetNetworkConfig` | `cfg: NetCfg, showAlertOnError: Boolean, ctrl: ChatCtrl?` | Apply network configuration (SOCKS proxy, timeouts, etc.) | [L1313](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1313) | +| `apiSetNetworkInfo` | `networkInfo: UserNetworkInfo` | Update network reachability information | [L1340](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1340) | +| `apiSetSettings` | `rh: Long?, type: ChatType, id: Long, settings: ChatSettings` | Update per-chat settings (notifications, favorites) | [L1333](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1333) | +| `apiStorageEncryption` | `currentKey: String, newKey: String` | Change the database encryption passphrase | [L999](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L999) | +| `testStorageEncryption` | `key: String, ctrl: ChatCtrl?` | Verify a database encryption key is correct | [L1006](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1006) | +| `testProtoServer` | `rh: Long?, server: String` | Test connectivity to a protocol server | [L1211](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1211) | +| `reconnectServer` | `rh: Long?, server: String` | Reconnect to a specific server | [L1326](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1326) | +| `reconnectAllServers` | `rh: Long?` | Reconnect to all servers | [L1331](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1331) | +| `apiSetChatUIThemes` | `rh: Long?, chatId: ChatId, themes: ThemeModeOverrides?` | Set per-chat UI theme overrides | [L1739](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1739) | +| `apiContactQueueInfo` | `rh: Long?, contactId: Long` | Get server queue diagnostics for a contact | [L1360](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1360) | +| `apiGroupMemberQueueInfo` | `rh: Long?, groupId: Long, groupMemberId: Long` | Get server queue diagnostics for a group member | [L1367](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1367) | + +### 2.9 Chat Tags + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiCreateChatTag` | `rh: Long?, tag: ChatTagData` | Create a new chat tag (folder/label) | [L1052](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1052) | +| `apiSetChatTags` | `rh: Long?, type: ChatType, id: Long, tagIds: List` | Assign tags to a chat | [L1060](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1060) | +| `apiDeleteChatTag` | `rh: Long?, tagId: Long` | Delete a chat tag | [L1068](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1068) | +| `apiUpdateChatTag` | `rh: Long?, tagId: Long, tag: ChatTagData` | Update a chat tag's name or emoji | [L1070](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1070) | +| `apiReorderChatTags` | `rh: Long?, tagIds: List` | Set the display order of chat tags | [L1072](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1072) | + +### 2.10 Server Operators + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `getServerOperators` | `rh: Long?` | Get server operator conditions detail | [L1219](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1219) | +| `setServerOperators` | `rh: Long?, operators: List` | Update the list of server operators | [L1226](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1226) | +| `getUserServers` | `rh: Long?` | Get the user's configured servers per operator | [L1233](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1233) | +| `setUserServers` | `rh: Long?, userServers: List` | Save user's configured servers per operator | [L1241](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1241) | +| `validateServers` | `rh: Long?, userServers: List` | Validate server configuration for errors | [L1253](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1253) | +| `getUsageConditions` | `rh: Long?` | Get current and accepted usage conditions | [L1261](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1261) | +| `setConditionsNotified` | `rh: Long?, conditionsId: Long` | Mark conditions as shown to user | [L1268](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1268) | +| `acceptConditions` | `rh: Long?, conditionsId: Long, operatorIds: List` | Accept usage conditions for operators | [L1275](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1275) | + +### 2.11 Archive + +| Command | Parameters | Description | Line | +|---------|-----------|-------------|------| +| `apiExportArchive` | `config: ArchiveConfig` | Export chat database to a ZIP archive | [L981](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L981) | +| `apiImportArchive` | `config: ArchiveConfig` | Import chat database from a ZIP archive | [L987](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L987) | +| `apiDeleteStorage` | _(none)_ | Delete all chat database storage | [L993](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L993) | + + + +`ArchiveConfig` ([SimpleXAPI.kt#L4162](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4162)): + +```kotlin +class ArchiveConfig( + val archivePath: String, + val disableCompression: Boolean? = null, + val parentTempDirectory: String? = null +) +``` + +--- + + + +## 3. Response Types + +All command responses are deserialized into the `API` sealed class ([SimpleXAPI.kt#L5975](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L5975)): + +```kotlin +sealed class API { + class Result(val remoteHostId: Long?, val res: CR) : API() + class Error(val remoteHostId: Long?, val err: ChatError) : API() +} +``` + + + +The `CR` sealed class ([SimpleXAPI.kt#L6114](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6114)) contains approximately 180 response variants. Key categories: + +| Category | Examples | Lines | +|----------|---------|-------| +| User | `ActiveUser`, `UsersList`, `UserPrivacy`, `UserProfileUpdated` | L6104-L6157 | +| Chat state | `ChatStarted`, `ChatRunning`, `ChatStopped`, `ApiChats`, `ApiChat` | L6106-L6110 | +| Tags | `ChatTags`, `TagsUpdated` | L6112, L6137 | +| Contacts | `Invitation`, `SentConfirmation`, `SentInvitation`, `ContactConnected`, `ContactDeleted` | L6138-L6165 | +| Messages | `NewChatItems`, `ChatItemUpdated`, `ChatItemsDeleted`, `ChatItemReaction`, `ForwardPlan` | L6176-L6184 | +| Groups | `GroupCreated`, `SentGroupInvitation`, `UserAcceptedGroupSent`, `GroupUpdated`, `GroupMembers` | L6186-L6219 | +| Files (receive) | `RcvFileAccepted`, `RcvFileStart`, `RcvFileComplete`, `RcvFileCancelled`, `RcvFileError` | L6221-L6232 | +| Files (send) | `SndFileStart`, `SndFileComplete`, `SndFileCancelled`, `SndFileCompleteXFTP` | L6234-L6244 | +| Calls | `CallInvitation`, `CallOffer`, `CallAnswer`, `CallExtraInfo`, `CallEnded` | L6246-L6251 | +| Remote host | `RemoteHostList`, `RemoteHostStarted`, `RemoteHostConnected`, `RemoteHostStopped` | L6255-L6262 | +| Remote ctrl | `RemoteCtrlList`, `RemoteCtrlFound`, `RemoteCtrlConnected`, `RemoteCtrlStopped` | L6264-L6269 | +| Encryption | `ContactPQAllowed`, `ContactPQEnabled` | L6271-L6272 | +| Misc | `CmdOk`, `ArchiveExported`, `ArchiveImported`, `AppSettingsR`, `VersionInfo` | L6274-L6283 | +| Fallback | `Response` (unknown type + raw JSON), `Invalid` (unparseable) | L6282-L6283 | + +Each `CR` subclass is annotated with `@Serializable @SerialName("jsonTag")` for polymorphic JSON deserialization. + +--- + +## 4. Event Types + +The chat core pushes asynchronous events through the same `CR` type hierarchy. The `startReceiver` coroutine ([SimpleXAPI.kt#L660](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L660)) continuously calls `chatRecvMsgWait` (blocking JNI), then dispatches each message to `processReceivedMsg` ([SimpleXAPI.kt#L2568](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2568)). + +Events handled in `processReceivedMsg` include: + +| Event | Description | +|-------|-------------| +| `ContactConnected` | A contact has completed the connection handshake | +| `ContactConnecting` | A contact connection is in progress | +| `ContactSndReady` | Contact's sending channel is ready | +| `ContactDeletedByContact` | A contact deleted their side of the conversation | +| `ReceivedContactRequest` | An incoming contact request arrived | +| `NewChatItems` | New messages received | +| `ChatItemUpdated` | A message was edited | +| `ChatItemsDeleted` | Messages were deleted | +| `ChatItemReaction` | A reaction was added/removed | +| `ChatItemsStatusesUpdated` | Delivery statuses updated | +| `GroupCreated` | A new group was created | +| `ReceivedGroupInvitation` | An invitation to join a group | +| `JoinedGroupMember` | A new member joined | +| `DeletedMember` / `DeletedMemberUser` | A member was removed | +| `LeftMember` | A member left voluntarily | +| `GroupUpdated` | Group profile changed | +| `MemberRole` | A member's role changed | +| `MemberBlockedForAll` | A member was blocked for all | +| `RcvFileStart` / `RcvFileComplete` / `RcvFileError` | File receive progress | +| `SndFileStart` / `SndFileComplete` / `SndFileError` | File send progress | +| `CallInvitation` / `CallOffer` / `CallAnswer` / `CallEnded` | Call signaling events | +| `ContactPQEnabled` | Post-quantum encryption status changed | +| `RemoteHostStopped` / `RemoteCtrlStopped` | Remote access session ended | +| `SubscriptionStatusEvt` | Connection subscription status changed | + +Each event triggers updates to `ChatModel` (reactive Compose state) and optionally fires platform notifications via `ntfManager`. + +--- + + + +## 5. Error Types + +### ChatError ([SimpleXAPI.kt#L6974](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L6974)) + +```kotlin +sealed class ChatError { + class ChatErrorChat(val errorType: ChatErrorType) // Application-level errors + class ChatErrorAgent(val agentError: AgentErrorType) // SMP/XFTP agent errors + class ChatErrorStore(val storeError: StoreError) // Database store errors + class ChatErrorDatabase(val databaseError: DatabaseError)// Database engine errors + class ChatErrorRemoteHost(val remoteHostError: ...) // Remote host errors + class ChatErrorRemoteCtrl(val remoteCtrlError: ...) // Remote controller errors + class ChatErrorInvalidJSON(val json: String) // JSON parsing failure +} +``` + +### ChatErrorType ([SimpleXAPI.kt#L7004](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7004)) + +Common application error codes (~70 variants): + +| Error | Meaning | +|-------|---------| +| `NoActiveUser` | No user profile is set as active | +| `UserExists` | Attempted to create a duplicate user | +| `InvalidDisplayName` | Display name contains invalid characters | +| `ChatNotStarted` / `ChatNotStopped` | Chat engine in wrong state | +| `InvalidConnReq` / `UnsupportedConnReq` | Bad or incompatible connection link | +| `ContactNotReady` / `ContactDisabled` | Contact in unusable state | +| `GroupUserRole` | Insufficient group permissions | +| `GroupNotJoined` | User has not joined the group | +| `FileNotFound` / `FileCancelled` / `FileAlreadyReceiving` | File transfer errors | +| `FileNotApproved` | File from unapproved relay server | +| `HasCurrentCall` / `NoCurrentCall` | Call state conflicts | +| `CommandError` / `InternalError` / `CEException` | Generic/internal errors | + +### StoreError ([SimpleXAPI.kt#L7168](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7168)) + +Database-level errors: `DuplicateName`, `UserNotFound`, `GroupNotFound`, `ChatItemNotFound`, `LargeMsg`, `UserContactLinkNotFound`, etc. + +### ArchiveError ([SimpleXAPI.kt#L7658](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7658)) + +```kotlin +sealed class ArchiveError { + class ArchiveErrorImport(val importError: String) + class ArchiveErrorFile(val file: String, val fileError: String) +} +``` + +--- + +## 6. Source Files + +| File | Purpose | Path | +|------|---------|------| +| SimpleXAPI.kt | API bridge: all `api*` functions, `CC`, `CR`, `ChatError` | `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | +| Core.kt | JNI externals, `initChatController`, `chatMigrateInit` | `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` | +| ChatModel.kt | Reactive UI state (`ChatModel` object) | `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` | +| DatabaseUtils.kt | `DBMigrationResult`, `MigrationError`, DB password helpers | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` | +| Files.kt | Platform-expect file path declarations | `common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt` | +| Files.android.kt | Android actual file paths | `common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt` | +| Files.desktop.kt | Desktop actual file paths | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt` | +| Cryptor.kt | Platform-expect encryption interface | `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` | +| Cryptor.android.kt | Android: AndroidKeyStore AES-GCM encryption | `common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt` | +| Cryptor.desktop.kt | Desktop: placeholder (no-op) encryption | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` | + +All paths are relative to `apps/multiplatform/`. diff --git a/apps/multiplatform/spec/architecture.md b/apps/multiplatform/spec/architecture.md new file mode 100644 index 0000000000..cfef4d06c2 --- /dev/null +++ b/apps/multiplatform/spec/architecture.md @@ -0,0 +1,423 @@ +# System Architecture + +## Table of Contents + +1. [Overview](#1-overview) +2. [Module Structure](#2-module-structure) +3. [JNI Bridge](#3-jni-bridge) +4. [App Lifecycle](#4-app-lifecycle) +5. [Event Streaming](#5-event-streaming) +6. [Platform Abstraction](#6-platform-abstraction) +7. [Source Files](#7-source-files) + +--- + +## 1. Overview + +The application is a three-layer system: + +``` ++------------------------------------------------------------------+ +| Compose UI (Views) | +| ChatListView, ChatView, ComposeView, SettingsView, CallView | ++------------------------------------------------------------------+ + | ^ + | user actions | Compose MutableState recomposition + v | ++------------------------------------------------------------------+ +| Application Logic Layer | +| ChatModel (state) ChatController (command dispatch) | +| AppPreferences NtfManager ThemeManager | ++------------------------------------------------------------------+ + | ^ + | sendCmd() | recvMsg() / processReceivedMsg() + v | ++------------------------------------------------------------------+ +| JNI Bridge (Core.kt) | +| external fun chatSendCmdRetry() external fun chatRecvMsgWait()| ++------------------------------------------------------------------+ + | ^ + | C FFI | C FFI + v | ++------------------------------------------------------------------+ +| Haskell Core (libsimplex / libapp-lib) | +| chat_ctrl handle SMP/XFTP protocols SQLite/PostgreSQL | ++------------------------------------------------------------------+ +``` + +**Data flow summary:** +1. User interacts with Compose UI. +2. View calls a `suspend fun api*()` method on `ChatController`. +3. `ChatController.sendCmd()` serializes the command to a JSON string and calls `chatSendCmdRetry()` (JNI). +4. The Haskell core processes the command and returns a JSON response string. +5. The response is deserialized to an `API` sealed class and returned to the caller. +6. Asynchronous events from the core (incoming messages, connection updates, call invitations) are delivered via a receiver coroutine that calls `chatRecvMsgWait()` in a loop and dispatches each event through `processReceivedMsg()`. + +--- + +## 2. Module Structure + +### Gradle Configuration + +Root: [`settings.gradle.kts`](../settings.gradle.kts#L22) +``` +include(":android", ":desktop", ":common") +``` + +### `:common` Module + +Build file: [`common/build.gradle.kts`](../common/build.gradle.kts#L14) + +``` +kotlin { + androidTarget() + jvm("desktop") +} +``` + +Source sets: + +| Source Set | Path | Purpose | +|---|---|---| +| `commonMain` | `common/src/commonMain/kotlin/` | All shared UI, models, platform abstractions | +| `androidMain` | `common/src/androidMain/kotlin/` | Android `actual` implementations | +| `desktopMain` | `common/src/desktopMain/kotlin/` | Desktop `actual` implementations | + +Key dependencies (from `commonMain`): +- `kotlinx-serialization-json` -- JSON codec for Haskell core communication +- `kotlinx-datetime` -- cross-platform date/time +- `multiplatform-settings` (russhwolf) -- `SharedPreferences` abstraction +- `kaml` -- YAML parsing (theme import/export) +- `boofcv-core` -- QR code scanning +- `jsoup` -- HTML parsing for link previews +- `moko-resources` -- cross-platform string/image resources +- `multiplatform-markdown-renderer` -- Markdown rendering in chat + +### `:android` Module + +Build file: [`android/build.gradle.kts`](../android/build.gradle.kts) + +Contains: +- `SimplexApp` (Application subclass) +- `MainActivity` (FragmentActivity) +- `SimplexService` (foreground Service) +- `NtfManager` (Android NotificationManager wrapper) +- `CallActivity` (dedicated activity for calls) + +### `:desktop` Module + +Build file: [`desktop/build.gradle.kts`](../desktop/build.gradle.kts) + +Contains: +- `main()` entry point +- `initHaskell()` -- loads native library and calls `initHS()` +- Window management (VLC library loading on Windows) + +--- + +## 3. JNI Bridge + +All JNI declarations reside in [`Core.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt). + + + + +### External Native Functions + +| # | Function | Signature | Line | Purpose | +|---|---|---|---|---| +| 1 | [`initHS()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L18) | `external fun initHS()` | 18 | Initialize GHC runtime system | +| 2 | [`pipeStdOutToSocket()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L20) | `external fun pipeStdOutToSocket(socketName: String): Int` | 20 | Redirect Haskell stdout to Android local socket for logging | +| 3 | [`chatMigrateInit()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L25) | `external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array` | 25 | Initialize database with migration; returns `[jsonResult, chatCtrl]` | +| 4 | [`chatCloseStore()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L26) | `external fun chatCloseStore(ctrl: ChatCtrl): String` | 26 | Close database store | +| 5 | [`chatSendCmdRetry()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L27) | `external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String` | 27 | Send command to core with retry count | +| 6 | [`chatSendRemoteCmdRetry()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L28) | `external fun chatSendRemoteCmdRetry(ctrl: ChatCtrl, rhId: Int, msg: String, retryNum: Int): String` | 28 | Send command to remote host | +| 7 | [`chatRecvMsg()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L29) | `external fun chatRecvMsg(ctrl: ChatCtrl): String` | 29 | Receive message (non-blocking) | +| 8 | [`chatRecvMsgWait()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L30) | `external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String` | 30 | Receive message with timeout (blocking up to `timeout` microseconds) | +| 9 | [`chatParseMarkdown()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L31) | `external fun chatParseMarkdown(str: String): String` | 31 | Parse markdown formatting | +| 10 | [`chatParseServer()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L32) | `external fun chatParseServer(str: String): String` | 32 | Parse SMP/XFTP server address | +| 11 | [`chatParseUri()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L33) | `external fun chatParseUri(str: String, safe: Int): String` | 33 | Parse SimpleX connection URI | +| 12 | [`chatPasswordHash()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L34) | `external fun chatPasswordHash(pwd: String, salt: String): String` | 34 | Hash password with salt | +| 13 | [`chatValidName()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L35) | `external fun chatValidName(name: String): String` | 35 | Validate/sanitize display name | +| 14 | [`chatJsonLength()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L36) | `external fun chatJsonLength(str: String): Int` | 36 | Get JSON-encoded string length | +| 15 | [`chatWriteFile()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L37) | `external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String` | 37 | Write encrypted file via core | +| 16 | [`chatReadFile()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L38) | `external fun chatReadFile(path: String, key: String, nonce: String): Array` | 38 | Read and decrypt file | +| 17 | [`chatEncryptFile()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L39) | `external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String` | 39 | Encrypt file on disk | +| 18 | [`chatDecryptFile()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L40) | `external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String` | 40 | Decrypt file on disk | + +**Total: 18 external native functions** (the `ChatCtrl` type alias at [line 23](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L23) is `Long`, representing the Haskell-side controller pointer). + + + + + +### Key Kotlin Functions in Core.kt + +| Function | Line | Purpose | +|---|---|---| +| [`initChatControllerOnStart()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L51) | 51 | Entry point called during app startup; launches `initChatController` in a long-running coroutine | +| [`initChatController()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L62) | 62 | Main initialization: DB migration via `chatMigrateInit`, error recovery (incomplete DB removal), sets file paths, loads active user, starts chat | +| [`chatInitTemporaryDatabase()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L190) | 190 | Creates a temporary database for migration scenarios | +| [`chatInitControllerRemovingDatabases()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L202) | 202 | Removes existing DBs and creates fresh controller (used during re-initialization) | +| [`showStartChatAfterRestartAlert()`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L222) | 222 | Shows confirmation dialog when chat was stopped and DB passphrase is stored | + + + +### initChatController Flow + +``` +initChatController(useKey, confirmMigrations, startChat) + | + +-- chatMigrateInit(dbPath, dbKey, confirm) // JNI -> Haskell + | returns [jsonResult, chatCtrl] + | + +-- if migration error and rerunnable: + | chatMigrateInit(dbPath, dbKey, confirm) // retry with user confirmation + | + +-- setChatCtrl(ctrl) // store controller handle + +-- apiSetAppFilePaths(...) // tell core about file dirs + +-- apiSetEncryptLocalFiles(...) + +-- apiGetActiveUser() -> currentUser + +-- getServerOperators() -> conditions + +-- if shouldImportAppSettings: apiGetAppSettings + importIntoApp + +-- if user exists and startChat confirmed: + | startChat(user) // starts receiver, API commands + +-- else if no user: + set onboarding stage, optionally startChatWithoutUser() +``` + +--- + +## 4. App Lifecycle + +### Android + +Entry: [`SimplexApp.onCreate()`](../android/src/main/java/chat/simplex/app/SimplexApp.kt#L47) + +``` +SimplexApp.onCreate() + +-- initHaskell(packageName) // Load native lib, pipe stdout, call initHS() + | +-- System.loadLibrary("app-lib") + | +-- pipeStdOutToSocket(packageName) + | +-- initHS() + +-- initMultiplatform() // Set up ntfManager, platform callbacks + +-- reconfigureBroadcastReceivers() + +-- runMigrations() // Theme migration, version code tracking + +-- initChatControllerOnStart() // -> initChatController() -> chatMigrateInit -> startChat +``` + +Activity: [`MainActivity.onCreate()`](../android/src/main/java/chat/simplex/app/MainActivity.kt#L32) + +``` +MainActivity.onCreate() + +-- processNotificationIntent(intent) // Handle OpenChat/AcceptCall from notifications + +-- processIntent(intent) // Handle VIEW intents (deep links) + +-- processExternalIntent(intent) // Handle SEND/SEND_MULTIPLE (share sheet) + +-- setContent { AppScreen() } // Compose UI entry point +``` + +Lifecycle callbacks in `SimplexApp` (implements `LifecycleEventObserver`): +- `ON_START`: refresh chat list from API if chat is running +- `ON_RESUME`: show background service notice, start `SimplexService` if configured + +### Desktop + +Entry: [`main()`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt#L21) + +``` +main() + +-- initHaskell() // Load native lib from resources dir, call initHS() + | +-- System.load(libapp-lib.so/dll/dylib) + | +-- initHS() + +-- runMigrations() + +-- setupUpdateChecker() + +-- initApp() // Set ntfManager, applyAppLocale, initChatControllerOnStart + +-- showApp() // Compose window with AppScreen() +``` + +[`showApp()`](../common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt#L33) creates a Compose `Window` with error recovery -- if a crash occurs, it closes the offending modal/view and re-opens the window. + +[`initApp()`](../common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt#L21) sets the `ntfManager` implementation (desktop notifications via `NtfManager` in `common/model/`) and calls `initChatControllerOnStart()`. + +--- + +## 5. Event Streaming + +### Receiver Coroutine + +[`ChatController.startReceiver()`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L660) launches a coroutine on `Dispatchers.IO` that continuously polls for events from the Haskell core: + +```kotlin +// SimpleXAPI.kt line 660 +private fun startReceiver() { + if (receiverJob != null || chatCtrl == null) return // guard against double-start + receiverJob = CoroutineScope(Dispatchers.IO).launch { + var releaseLock: (() -> Unit) = {} + while (isActive) { + val ctrl = chatCtrl + if (ctrl == null) { stopReceiver(); break } // chatCtrl became null + try { + val release = releaseLock + launch { delay(30000); release() } // release previous wake lock after 30s + val msg = recvMsg(ctrl) // calls chatRecvMsgWait with 300s timeout + releaseLock = getWakeLock(timeout = 60000) // acquire wake lock (60s timeout) + if (msg != null) { + val finished = withTimeoutOrNull(60_000L) { + processReceivedMsg(msg) + messagesChannel.trySend(msg) + } + if (finished == null) { + Log.e(TAG, "Timeout processing: " + msg.responseType) + } + } + } catch (e: Exception) { + Log.e(TAG, "recvMsg/processReceivedMsg exception: " + e.stackTraceToString()) + } catch (e: Throwable) { + Log.e(TAG, "recvMsg/processReceivedMsg throwable: " + e.stackTraceToString()) + } + } + } +} +``` + +### Message Reception + +[`recvMsg()`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L829) calls `chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)` where `MESSAGE_TIMEOUT = 300_000_000` microseconds (300 seconds). Returns `null` on timeout (empty string from Haskell), otherwise deserializes the JSON response to an `API` instance. + +### Command Sending + +[`sendCmd()`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L804) runs on `Dispatchers.IO`, serializes the command via `CC.cmdString`, calls `chatSendCmdRetry()` (or `chatSendRemoteCmdRetry()` for remote hosts), deserializes the response, and logs terminal items. + +### Event Processing + +[`processReceivedMsg()`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2568) is a large `when` block that dispatches on the `CR` (ChatResponse) type: + +- `CR.ContactConnected` -- update contact in `ChatModel` +- `CR.NewChatItems` -- add items to chat, trigger notifications +- `CR.RcvCallInvitation` -- add to `callInvitations`, trigger call UI +- `CR.ChatStopped` -- set `chatRunning = false` +- `CR.GroupMemberConnected`, `CR.GroupMemberUpdated`, etc. -- update group state +- Many more event types for connection status, file transfers, SMP relay events, etc. + +### Wake Lock + +On Android, the receiver acquires a wake lock via [`getWakeLock(timeout)`](../common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt#L3) (expect function) after each received message with a 60-second timeout. The previous iteration's wake lock is released after a 30-second delay, ensuring overlap so the CPU does not sleep between messages. + +--- + +## 6. Platform Abstraction + +### expect/actual Pattern + +The `commonMain` source set declares `expect` functions and classes. Each platform source set provides `actual` implementations. + +Examples from platform files: + +| expect Declaration | File | Line | +|---|---|---| +| `expect val appPlatform: AppPlatform` | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L20) | 20 | +| `expect val deviceName: String` | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L22) | 22 | +| `expect fun isAppVisibleAndFocused(): Boolean` | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L24) | 24 | +| `expect val dataDir: File` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L18) | 18 | +| `expect val tmpDir: File` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L19) | 19 | +| `expect val filesDir: File` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L20) | 20 | +| `expect val appFilesDir: File` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L21) | 21 | +| `expect val dbAbsolutePrefixPath: String` | [`Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L24) | 24 | +| `expect fun showToast(text: String, timeout: Long)` | [`UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt#L6) | 6 | +| `expect fun hideKeyboard(view: Any?, clearFocus: Boolean)` | [`UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt#L16) | 16 | +| `expect fun getKeyboardState(): State` | [`UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt#L15) | 15 | +| `expect fun allowedToShowNotification(): Boolean` | [`Notifications.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt#L3) | 3 | +| `expect class VideoPlayer` | [`VideoPlayer.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt#L25) | 25 | +| `expect class RecorderNative` | [`RecAndPlay.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt#L17) | 17 | +| `expect val cryptor: CryptorInterface` | [`Cryptor.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt#L9) | 9 | +| `expect fun base64ToBitmap(base64ImageString: String): ImageBitmap` | [`Images.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt#L17) | 17 | +| `expect fun getWakeLock(timeout: Long): (() -> Unit)` | [`SimplexService.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt#L3) | 3 | +| `expect class GlobalExceptionsHandler` | [`UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt#L24) | 24 | +| `expect fun UriHandler.sendEmail(subject: String, body: CharSequence)` | [`Share.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt#L7) | 7 | +| `expect fun ClipboardManager.shareText(text: String)` | [`Share.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt#L9) | 9 | +| `expect fun shareFile(text: String, fileSource: CryptoFile)` | [`Share.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt#L10) | 10 | + +### PlatformInterface Callback Object + +[`PlatformInterface`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L15) is an interface with default no-op implementations. It is assigned at runtime by each platform entry point: + +- **Android**: assigned in [`SimplexApp.initMultiplatform()`](../android/src/main/java/chat/simplex/app/SimplexApp.kt#L187) (line 187) +- **Desktop**: assigned in [`Main.kt initHaskell()`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt#L50) (line 50) + +The global variable is declared at [`Platform.kt line 50`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L50): +```kotlin +var platform: PlatformInterface = object : PlatformInterface {} +``` + +#### PlatformInterface Callbacks + +| Callback | Default | Android Implementation | +|---|---|---| +| `androidServiceStart()` | no-op | Start `SimplexService` foreground service | +| `androidServiceSafeStop()` | no-op | Stop `SimplexService` | +| `androidCallServiceSafeStop()` | no-op | Stop `CallService` | +| `androidNotificationsModeChanged(mode)` | no-op | Toggle receivers, start/stop service | +| `androidChatStartedAfterBeingOff()` | no-op | Start service or schedule periodic worker | +| `androidChatStopped()` | no-op | Cancel workers, stop service | +| `androidChatInitializedAndStarted()` | no-op | Show background service notice, start service | +| `androidIsBackgroundCallAllowed()` | `true` | Check battery restriction | +| `androidSetNightModeIfSupported()` | no-op | Set `UiModeManager` night mode | +| `androidSetStatusAndNavigationBarAppearance(...)` | no-op | Configure system bar colors/appearance | +| `androidStartCallActivity(acceptCall, rhId, chatId)` | no-op | Launch `CallActivity` | +| `androidPictureInPictureAllowed()` | `true` | Check PiP permission via AppOps | +| `androidCallEnded()` | no-op | Destroy call WebView | +| `androidRestartNetworkObserver()` | no-op | Restart `NetworkObserver` | +| `androidCreateActiveCallState()` | empty `Closeable` | Create `ActiveCallState` | +| `androidIsXiaomiDevice()` | `false` | Check device brand | +| `androidApiLevel` | `null` | `Build.VERSION.SDK_INT` | +| `androidLockPortraitOrientation()` | no-op | Lock to `SCREEN_ORIENTATION_PORTRAIT` | +| `androidAskToAllowBackgroundCalls()` | `true` | Show battery restriction dialog | +| `desktopShowAppUpdateNotice()` | no-op | Show update notice (Desktop only) | + +--- + +## 7. Source Files + +### Core Infrastructure + +| File | Path | Key Contents | +|---|---|---| +| Core.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt) | JNI externals, `initChatController`, `chatInitTemporaryDatabase` | +| SimpleXAPI.kt | [`common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | `ChatController`, `AppPreferences`, `startReceiver`, `sendCmd`, `recvMsg`, `processReceivedMsg`, all `api*` functions | +| ChatModel.kt | [`common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt) | `ChatModel` singleton, `ChatsContext`, `Chat`, `ChatInfo`, `ChatItem` and all domain types | +| App.kt | [`common/src/commonMain/kotlin/chat/simplex/common/App.kt`](../common/src/commonMain/kotlin/chat/simplex/common/App.kt) | `AppScreen()`, `MainScreen()` | + +### Platform Layer + +| File | Path | Key Contents | +|---|---|---| +| Platform.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt) | `PlatformInterface`, global `platform` var | +| AppCommon.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt) | `AppPlatform`, `runMigrations()` | +| AppCommon.android.kt | [`common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt`](../common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt) | `initHaskell()`, `androidAppContext` | +| AppCommon.desktop.kt | [`common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt) | `initApp()`, desktop NtfManager setup | +| Files.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) | `expect val dataDir/tmpDir/filesDir/dbAbsolutePrefixPath` | +| NtfManager.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt) | `abstract class NtfManager` | +| Notifications.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt) | `expect fun allowedToShowNotification()` | +| UI.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt) | `showToast`, `hideKeyboard`, `GlobalExceptionsHandler` | +| Share.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt) | `shareText`, `shareFile`, `openFile` | +| VideoPlayer.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt) | `VideoPlayerInterface`, `expect class VideoPlayer` | +| RecAndPlay.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt) | `RecorderInterface`, `AudioPlayerInterface` | +| Cryptor.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt) | `CryptorInterface` | +| Images.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt) | `base64ToBitmap`, `resizeImageToStrSize` | +| SimplexService.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/SimplexService.kt) | `expect fun getWakeLock()` | + +### Entry Points + +| File | Path | Key Contents | +|---|---|---| +| SimplexApp.kt | [`android/src/main/java/chat/simplex/app/SimplexApp.kt`](../android/src/main/java/chat/simplex/app/SimplexApp.kt) | Android Application class, lifecycle observer | +| MainActivity.kt | [`android/src/main/java/chat/simplex/app/MainActivity.kt`](../android/src/main/java/chat/simplex/app/MainActivity.kt) | Android main activity | +| SimplexService.kt | [`android/src/main/java/chat/simplex/app/SimplexService.kt`](../android/src/main/java/chat/simplex/app/SimplexService.kt) | Android foreground service | +| Main.kt | [`desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt`](../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt) | Desktop `main()` | +| DesktopApp.kt | [`common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt) | `showApp()`, `SimplexWindowState` | + +### Theme + +| File | Path | Key Contents | +|---|---|---| +| ThemeManager.kt | [`common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt) | Theme resolution, system/light/dark/custom, per-user overrides | diff --git a/apps/multiplatform/spec/client/chat-list.md b/apps/multiplatform/spec/client/chat-list.md new file mode 100644 index 0000000000..b0f3750659 --- /dev/null +++ b/apps/multiplatform/spec/client/chat-list.md @@ -0,0 +1,314 @@ +# Chat List Specification + +Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt` + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatListView Composable](#2-chatlistview-composable) +3. [Data Sources](#3-data-sources) +4. [Filter System](#4-filter-system) +5. [Chat Preview](#5-chat-preview) +6. [ChatListNavLinkView](#6-chatlistnavlinkview) +7. [Tag System](#7-tag-system) +8. [UserPicker](#8-userpicker) +9. [Source Files](#9-source-files) + +--- + +## Executive Summary + +The Chat List is the landing screen of SimpleX Chat, rendering all conversations for the active user. Built around `ChatListView` (line 126 in `ChatListView.kt`), it provides a searchable, filterable `LazyColumn` of chat previews with a toolbar, tag-based filtering, and a user-switching side panel. The view adapts between one-hand UI mode (toolbar at bottom, reversed list) and standard mode (toolbar at top). Search also accepts SimpleX links for direct connection. + +--- + +## 1. Overview + +``` +ChatListView +|-- ChatListToolbar (top or bottom app bar) +| |-- UserProfileButton (opens UserPicker) +| |-- Title ("Your chats") +| |-- SubscriptionStatusIndicator +| +-- NewChatButton / StoppedIndicator +|-- ChatListWithLoadingScreen +| |-- ChatList (LazyColumnWithScrollBar) +| | |-- Spacer (top/bottom padding) +| | |-- stickyHeader +| | | |-- ChatListSearchBar (search input + filter toggle) +| | | +-- TagsView (preset + custom tag chips) +| | |-- ChatListNavLinkView[] (per-chat row items) +| | +-- ChatListFeatureCards (one-hand UI card, address card) +| +-- EmptyState text +|-- NewChatSheetFloatingButton (FAB, standard mode only) +|-- UserPicker (slide-in panel, Android) ++-- ActiveCallInteractiveArea (desktop, in-call banner) +``` + +--- + + + +## 2. ChatListView Composable + +**Location:** [`ChatListView.kt#L127`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L127) + +```kotlin +fun ChatListView( + chatModel: ChatModel, + userPickerState: MutableStateFlow, + setPerformLA: (Boolean) -> Unit, + stopped: Boolean +) +``` + +### Initialization + +- Shows "What's New" modal on first launch after update (line ~130), with a 1-second delay. +- On desktop, closing a chat resets audio/video players (line ~138). + +### Layout Modes + +The `oneHandUI` preference (`appPrefs.oneHandUI.state`) controls the layout: + +| Mode | Toolbar Position | List Direction | FAB | Search/Tags Position | +|---|---|---|---|---| +| **Standard** (`oneHandUI = false`) | Top | Top-to-bottom | Bottom-right FAB | Below toolbar | +| **One-hand** (`oneHandUI = true`) | Bottom | Bottom-to-top (reversed) | Integrated in toolbar | Above toolbar | + +### State + +| State | Type | Purpose | +|---|---|---| +| `searchText` | `MutableState` | Search query (saved across recomposition) | +| `listState` | `LazyListState` | Scroll position (persisted in `lazyListState` var) | +| `oneHandUI` | `State` | One-hand UI mode toggle | + +### Android-specific + +- `SetNotificationsModeAdditions`: Notification permission setup (line ~184). +- `UserPicker`: Overlay side panel for user switching (line ~192). + +--- + +## 3. Data Sources + +| Source | Location | Description | +|---|---|---| +| `chatModel.chats` | `ChatModel.chatsContext.chats` | Full list of `Chat` objects for the active user | +| `chatModel.activeChatTagFilter` | `ChatModel.activeChatTagFilter` | Currently active filter (`PresetTag`, `UserTag`, or `Unread`) | +| `chatModel.userTags` | `ChatModel.userTags` | User-created custom tags | +| `chatModel.presetTags` | `ChatModel.presetTags` | Map of `PresetTagKind` to count | +| `chatModel.unreadTags` | `ChatModel.unreadTags` | Map of tag ID to unread count | +| `chatModel.chatId` | `ChatModel.chatId` | Currently selected chat ID (highlights row) | +| `chatModel.currentUser` | `ChatModel.currentUser` | Active user profile | +| `chatModel.users` | `ChatModel.users` | All user profiles (for UserPicker) | +| `chatModel.showChatPreviews` | `ChatModel.showChatPreviews` | Privacy toggle for message previews | + +--- + +## 4. Filter System + +### Active Filter Types + +Defined as sealed class `ActiveFilter` (line ~51): + +```kotlin +sealed class ActiveFilter { + data class PresetTag(val tag: PresetTagKind) : ActiveFilter() + data class UserTag(val tag: ChatTag) : ActiveFilter() + data object Unread : ActiveFilter() +} +``` + +### PresetTagKind Enum + +| Value | Description | +|---|---| +| `GROUP_REPORTS` | Groups with active reports (moderator-visible) | +| `FAVORITES` | Chats marked as favorite | +| `CONTACTS` | Direct (1:1) chats | +| `GROUPS` | Group chats | +| `BUSINESS` | Business-type chats | +| `NOTES` | Local note folders | + +### Search Filtering + +The `filteredChats` function (line ~1188) applies filters in this order: + +1. **SimpleX link match:** If a pasted link resolved to a known contact/group, show only that chat. +2. **Text search:** Case-insensitive match against `chat.chatInfo.chatViewName`, `chat.chatInfo.fullName`, and `chat.chatInfo.localAlias`. +3. **Active filter:** + - `PresetTag`: Matches chat type and characteristics (e.g., `CONTACTS` filters `ChatInfo.Direct`, `GROUPS` filters `ChatInfo.Group`). + - `UserTag`: Matches chats whose `chatTags` contain the tag ID. + - `Unread`: Matches chats with `unreadCount > 0` or `unreadChat == true`. + +### Search Bar + +`ChatListSearchBar` (line ~611) provides: +- Text input with search icon. +- SimpleX link detection: When a pasted string contains a single SimpleX link, it triggers `planAndConnect` for connection, suppressing normal search. +- Unread filter toggle button (right side, when search is empty). + +--- + + + +## 5. Chat Preview + +**Location:** [`ChatPreviewView.kt#L40`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt#L40) + +```kotlin +fun ChatPreviewView( + chat: Chat, + showChatPreviews: Boolean, + chatModelDraft: ComposeState?, + chatModelDraftChatId: ChatId?, + currentUserProfileDisplayName: String?, + disabled: Boolean, + linkMode: SimplexLinkMode, + inProgress: Boolean, + progressByTimeout: Boolean, + defaultClickAction: () -> Unit +) +``` + +### Layout + +Each chat preview row contains: + +| Element | Position | Content | +|---|---|---| +| Profile image | Left | `ChatInfoImage` with overlay icons for inactive contacts/groups | +| Title row | Top-right of image | Chat name (bold), verified shield (direct), timestamp | +| Preview row | Below title | Last message preview or draft indicator, unread badge | +| Unread badge | Right | Circular badge with count, or dot for muted chats | + +### Draft Display + +When `chatModelDraftChatId` matches the chat ID, the preview shows a draft indicator (pencil icon) with the draft message text instead of the last chat item. + +### Inactive Indicators + +- Inactive contacts: cancel icon overlay on profile image. +- Left/removed/deleted groups: cancel icon overlay. + +--- + + + +## 6. ChatListNavLinkView + +**Location:** [`ChatListNavLinkView.kt#L37`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt#L37) + +Routes each chat to the appropriate click action and context menu based on `chat.chatInfo`: + +| ChatInfo Type | Click Action | Context Menu | +|---|---|---| +| `ChatInfo.Direct` | `directChatAction` (opens chat) | `ContactMenuItems`: mark read/unread, mute, favorite, tag, clear, delete | +| `ChatInfo.Group` | `groupChatAction` (opens chat or joins) | `GroupMenuItems`: mark read/unread, mute, favorite, tag, clear, leave, delete | +| `ChatInfo.Local` | `noteFolderChatAction` (opens notes) | `NoteFolderMenuItems`: mark read, clear, delete | +| `ChatInfo.ContactRequest` | `contactRequestAlertDialog` (accept/reject) | `ContactRequestMenuItems`: reject | +| `ChatInfo.ContactConnection` | Sets `chatModel.chatId` (opens connection info) | `ContactConnectionMenuItems`: delete | +| `ChatInfo.InvalidJSON` | Sets `chatModel.chatId` | No menu | + +### Selection Highlight + +On desktop, the currently selected chat (`chatModel.chatId.value == chat.id`) receives a highlight background. `nextChatSelected` state is used to suppress the bottom divider when the next chat in the list is selected. + +--- + +## 7. Tag System + +### TagsView + +**Location:** [`ChatListView.kt#L929`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L929) + +Renders a horizontally scrollable row of tag chips (via `TagsRow`, which is a platform-specific `expect` composable). + +Layout logic: +- If there are more than 1 collapsible preset tags and the total tag count exceeds 3, preset tags collapse into a `CollapsedTagsFilterView` dropdown. +- Otherwise, each preset tag renders as an `ExpandedTagFilterView` chip. +- User tags render as individual chips with emoji or label icon, bold when active. +- A "+" button at the end opens `TagListEditor` for creating new tags. + +### Tag Interactions + +- **Single tap:** Toggles the tag filter on `chatModel.activeChatTagFilter`. +- **Long press / right-click (user tags):** Opens dropdown menu with edit/delete/reorder options. +- **Unread dot:** Shown on tags that have chats with unread messages. + + + +### TagListView + +**Location:** [`TagListView.kt#L48`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt#L48) + +Full-screen tag management view opened from the "+" button or long-press menu. + +```kotlin +fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) +``` + +- Displays all user tags in a `LazyColumnWithScrollBar`. +- Supports drag-and-drop reordering via `rememberDragDropState` (calls `apiReorderChatTags`). +- Each tag row shows emoji/icon, name, chat count, and a checkbox if opened for a specific chat (to assign/unassign tags). +- "Create list" button opens `TagListEditor` modal. + +--- + + + +## 8. UserPicker + +**Location:** [`UserPicker.kt#L46`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt#L46) + +```kotlin +fun UserPicker( + chatModel: ChatModel, + userPickerState: MutableStateFlow, + setPerformLA: (Boolean) -> Unit +) +``` + +### Behavior + +- **Android:** Renders as a slide-up overlay panel on the chat list, triggered by tapping the user profile button in the toolbar. +- **Desktop:** Rendered inline in the left column of `DesktopScreen`, always accessible. +- Closes automatically when any `ModalManager.start` modal opens. + +### Content + +| Section | Content | +|---|---| +| **Active user** | Profile image, display name, "active" indicator | +| **Other users** | List of non-hidden user profiles sorted by `activeOrder`; tapping switches user | +| **Remote hosts** | Connected remote devices (desktop linking) | +| **Settings** | Opens `SettingsView` modal | +| **Color mode** | `ColorModeSwitcher` for theme toggle | +| **Add profile** | Opens `CreateProfile` flow | +| **Lock** | Locks app (calls `AppLock.setPerformLA`) | + +### State Machine + +Uses `AnimatedViewState` (`GONE`, `VISIBLE`, `HIDING`) with a `MutableStateFlow` to coordinate animation between the parent screen and the picker overlay. + +--- + +## 9. Source Files + +| File | Description | +|---|---| +| `ChatListView.kt` | Main chat list view, toolbar, search, tags, filtering | +| `ChatListNavLinkView.kt` | Per-chat row routing and context menus | +| `ChatPreviewView.kt` | Chat preview row layout (image, title, last message) | +| `ChatHelpView.kt` | Empty-state help content | +| `ContactConnectionView.kt` | Pending connection preview row | +| `ContactRequestView.kt` | Contact request preview row | +| `ServersSummaryView.kt` | Server connection status summary | +| `ShareListNavLinkView.kt` | Share target list row (forwarding) | +| `ShareListView.kt` | Share target list (forwarding flow) | +| `TagListView.kt` | Tag management and assignment view | +| `UserPicker.kt` | User switching side panel | diff --git a/apps/multiplatform/spec/client/chat-view.md b/apps/multiplatform/spec/client/chat-view.md new file mode 100644 index 0000000000..2819b1e751 --- /dev/null +++ b/apps/multiplatform/spec/client/chat-view.md @@ -0,0 +1,324 @@ +# Chat View Specification + +Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt` + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatView Composable](#2-chatview-composable) +3. [Message List](#3-message-list) +4. [ChatItemView](#4-chatitemview) +5. [Message Types](#5-message-types) +6. [Context Menu Actions](#6-context-menu-actions) +7. [ChatInfoView](#7-chatinfoview) +8. [GroupChatInfoView](#8-groupchatinfoview) +9. [Source Files](#9-source-files) + +--- + +## Executive Summary + +The Chat View is the primary message display and interaction surface in SimpleX Chat. It is built around the `ChatView` composable (line ~96 in `ChatView.kt`), which orchestrates a `ChatLayout` containing a reverse-scrolling `LazyColumn` of `ChatItemView` items and a `ComposeView` for message input. The view supports direct chats, group chats, local notes, and contact connections, with per-chat theming, search/filter, multi-select, and side-panel info modals. Message rendering is delegated to type-specific composables in the `views/chat/item/` package. + +--- + +## 1. Overview + +``` +ChatView +|-- ChatLayout +| |-- ChatInfoToolbar (top/bottom app bar with back, title, call, search, menu) +| |-- SupportChatsCountToolbar (reports/support banner, group only) +| |-- ChatItemsList (LazyColumnWithScrollBar, reverse layout) +| | |-- ChatViewListItem +| | | |-- DateSeparator +| | | |-- MemberNameAndRole (group received messages) +| | | |-- MemberImage (group received messages) +| | | +-- ChatItemView (message type routing) +| | |-- ChatBannerView (first item: chat profile banner) +| | +-- FloatingButtons (scroll-to-bottom, unread counter) +| |-- ComposeView (message composition area) +| | |-- ContextItemView (reply/edit/forward/report indicator) +| | |-- previewView (link/media/voice/file preview) +| | +-- SendMsgView (text input + send/voice/timed buttons) +| |-- GroupMentions (mention autocomplete popup) +| |-- CommandsMenuView (bot commands popup) +| +-- ChooseAttachmentView (bottom sheet for attachment type) +|-- ChatInfoView (contact info, end modal) ++-- GroupChatInfoView (group management, end modal) +``` + +--- + + + +## 2. ChatView Composable + +**Location:** [`ChatView.kt#L97`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L97) + +```kotlin +fun ChatView( + chatsCtx: ChatModel.ChatsContext, + staleChatId: State, + scrollToItemId: MutableState, + onComposed: suspend (chatId: String) -> Unit +) +``` + +### State Management + +| State Variable | Type | Purpose | +|---|---|---| +| `showSearch` | `MutableState` | Controls search bar visibility | +| `searchText` | `MutableState` | Current search query text | +| `composeState` | `MutableState` | Full compose area state (message, preview, context, mentions) | +| `attachmentOption` | `MutableState` | Selected attachment type from bottom sheet | +| `selectedChatItems` | `MutableState?>` | Multi-select mode item IDs; `null` = selection off | +| `showCommandsMenu` | `MutableState` | Bot commands menu visibility | +| `contentFilter` | `MutableState` | Active content type filter (images, videos, etc.) | +| `availableContent` | `MutableState>` | Content types available in this chat | +| `activeChat` | `State` | Derived from `chatModel.chats` matching `staleChatId` | +| `unreadCount` | `State` | Unread message count derived from chat stats | + +### Chat Loading + +On chat ID change (via `snapshotFlow` on `chatModel.chatId.value`, line ~162): + +1. Marks unread chat as read (`markUnreadChatAsRead`) +2. Clears group members state +3. Resets search, content filter, and selection +4. Fetches available content types (`updateAvailableContent`) +5. For direct chats, loads contact info and connection stats +6. For groups with pending membership, opens member support chat + +### Chat Type Routing + +The outer `when (chatInfo)` (line ~229) branches: + +| ChatInfo Type | Behavior | +|---|---| +| `ChatInfo.Direct`, `ChatInfo.Group`, `ChatInfo.Local` | Full `ChatLayout` with compose, search, reactions, per-chat theme | +| `ChatInfo.ContactConnection` | `ModalView` wrapping `ContactConnectionInfoView` | +| `ChatInfo.InvalidJSON` | `ModalView` with raw JSON display and share button | + +--- + +## 3. Message List + +**Location:** [`ChatView.kt#L1592`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt#L1592) (`ChatItemsList` composable) + +The message list is a `LazyColumnWithScrollBar` with `reverseLayout = true`, meaning index 0 is the newest message at the bottom of the screen. + +### Key Behaviors + +- **Merged Items:** Messages are grouped via `MergedItems.create()` (line ~1653), which collapses consecutive similar system events into expandable groups. Revealed state is tracked in `revealedItems`. +- **Pagination:** `PreloadItems` triggers `loadMessages` with `ChatPagination.Before` (older) or `ChatPagination.Last` (newer) when the user scrolls near list boundaries. +- **Scroll To Item:** `scrollToItem` lambda supports animated scrolling to a specific item ID, used by search result taps and quoted message navigation. +- **Unread Marking:** `MarkItemsReadAfterDelay` composable marks newly visible received items as read after a brief delay. +- **Date Separators:** `DateSeparator` composable renders between messages when the date changes (via `ItemSeparation.date`). +- **Swipe to Reply:** `SwipeToDismiss` modifier on each item (EndToStart direction, 30dp threshold) sets `ComposeContextItem.QuotedItem`. +- **Selection Mode:** When `selectedChatItems` is non-null, a checkbox overlay appears on each item; a full-width clickable overlay toggles selection. + +### Item Layout (ChatViewListItem) + +- **Group received messages** with `showAvatar = true`: Column layout with `MemberNameAndRole` header, `MemberImage` (clickable to `showMemberInfo`), and message bubble. +- **Group received without avatar:** Indented to align with avatar-bearing messages. +- **Sent messages (group or direct):** Right-aligned with larger start padding. +- **Direct messages:** Symmetric padding (76dp opposite side). + +--- + + + +## 4. ChatItemView + +**Location:** [`item/ChatItemView.kt#L66`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt#L66) + +```kotlin +fun ChatItemView( + chatsCtx, rhId, chat, cItem, composeState, imageProvider, + useLinkPreviews, linkMode, revealed, highlighted, hoveredItemId, + range, selectedChatItems, searchIsNotBlank, fillMaxWidth, + selectChatItem, deleteMessage, deleteMessages, archiveReports, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, + openDirectChat, forwardItem, scrollToItem, scrollToItemId, + scrollToQuotedItemFromItem, setReaction, showItemDetails, + reveal, showMemberInfo, showChatInfo, developerTools, showViaProxy, + showTimestamp, itemSeparation, ... +) +``` + +The composable routes based on `cItem.content` and `cItem.meta.itemDeleted`: + +- **Deleted items** -> `DeletedItemView` or `MarkedDeletedItemView` +- **Message content** (`SndMsgContent`, `RcvMsgContent`) -> `FramedItemView` or specialized views depending on `msgContent` type +- **Call items** -> `CICallItemView` +- **Integrity/decryption errors** -> `IntegrityErrorItemView`, `CIRcvDecryptionError` +- **Group invitations** -> `CIGroupInvitationView` +- **Events** (group/direct/connection events) -> `CIEventView` +- **Feature changes** -> `CIChatFeatureView`, `CIFeaturePreferenceView` +- **E2EE info** -> `CIEventView` +- **Chat banner** -> handled at list level, not in `ChatItemView` +- **Invalid JSON** -> `CIInvalidJSONView` + +### Reactions + +`ChatItemReactions` row renders below each message bubble, showing emoji reaction counts. Tapping own reactions removes them; tapping others' opens a member list dropdown. + +### Context Menu + +Long-press or right-click opens a dropdown menu with context-sensitive actions (see section 6). + +--- + +## 5. Message Types + +| CIContent Variant | MsgContent Type | View Composable | Source File | +|---|---|---|---| +| `SndMsgContent` / `RcvMsgContent` | `MCText` | `FramedItemView` -> `TextItemView` or `EmojiItemView` | `TextItemView.kt`, `EmojiItemView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCLink` | `FramedItemView` (with link preview) | `FramedItemView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCImage` | `CIImageView` (inside `FramedItemView`) | `CIImageView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCVideo` | `CIVideoView` (inside `FramedItemView`) | `CIVideoView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCVoice` | `CIVoiceView` | `CIVoiceView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCFile` | `CIFileView` | `CIFileView.kt` | +| `SndMsgContent` / `RcvMsgContent` | `MCReport` | `FramedItemView` (with report styling) | `FramedItemView.kt` | +| `SndCall` / `RcvCall` | -- | `CICallItemView` | `CICallItemView.kt` | +| `RcvIntegrityError` | -- | `IntegrityErrorItemView` | `IntegrityErrorItemView.kt` | +| `RcvDecryptionError` | -- | `CIRcvDecryptionError` | `CIRcvDecryptionError.kt` | +| `RcvGroupInvitation` / `SndGroupInvitation` | -- | `CIGroupInvitationView` | `CIGroupInvitationView.kt` | +| `RcvDirectEventContent` | -- | `CIEventView` | `CIEventView.kt` | +| `RcvGroupEventContent` / `SndGroupEventContent` | -- | `CIEventView` | `CIEventView.kt` | +| `RcvConnEventContent` / `SndConnEventContent` | -- | `CIEventView` | `CIEventView.kt` | +| `RcvChatFeature` / `SndChatFeature` | -- | `CIChatFeatureView` | `CIChatFeatureView.kt` | +| `RcvChatPreference` / `SndChatPreference` | -- | `CIFeaturePreferenceView` | `CIFeaturePreferenceView.kt` | +| `RcvGroupFeature` / `SndGroupFeature` | -- | `CIChatFeatureView` | `CIChatFeatureView.kt` | +| `SndModerated` / `RcvModerated` / `RcvBlocked` | -- | `MarkedDeletedItemView` | `MarkedDeletedItemView.kt` | +| `SndDirectE2EEInfo` / `RcvDirectE2EEInfo` | -- | `CIEventView` | `CIEventView.kt` | +| `SndGroupE2EEInfo` / `RcvGroupE2EEInfo` | -- | `CIEventView` | `CIEventView.kt` | +| `RcvChatFeatureRejected` / `RcvGroupFeatureRejected` | -- | `CIChatFeatureView` | `CIChatFeatureView.kt` | +| `ChatBanner` | -- | `ChatBannerView` (inline in `ChatItemsList`) | `ChatView.kt` | +| `InvalidJSON` | -- | `CIInvalidJSONView` | `CIInvalidJSONView.kt` | +| `CIMemberCreatedContact` | -- | `CIMemberCreatedContactView` | `CIMemberCreatedContactView.kt` | + +--- + +## 6. Context Menu Actions + +Context menu actions are built dynamically in `ChatItemView` based on message type, direction, chat type, and feature flags. + +| Action | Condition | Effect | +|---|---|---| +| **Reply** | Message content (not event/deleted), not local notes | Sets `ComposeContextItem.QuotedItem` | +| **Edit** | Sent message, editable (`meta.editable`), text/link content | Sets `ComposeContextItem.EditingItem` | +| **Delete for me** | Any deletable item | `apiDeleteChatItems` with `cidmInternal` mode | +| **Delete for everyone** | Sent + within time window, or moderator privilege | `apiDeleteChatItems` with `cidmBroadcast` mode | +| **Moderate** | Group moderator + received message | `apiDeleteMemberChatItems` | +| **Forward** | Message content, not live message | Opens share sheet via `SharedContent.Forward` | +| **Select** | Any selectable item | Enters multi-select mode (`selectedChatItems`) | +| **React** | Message content, reactions enabled | Opens emoji picker; calls `apiChatItemReaction` | +| **Report** | Received group message, reports enabled | Sets `ComposeContextItem.ReportedItem` with reason | +| **Info** | Any message | Opens `ChatItemInfoView` in end modal | +| **Copy** | Text content present | Copies text to clipboard | +| **Save** | Image/video/file with completed download | Saves media to device | +| **Open** | File with completed download | Opens file with system handler | +| **Reveal / Hide** | Part of a merged group; expanded or collapsed | Toggles `revealedItems` state | + +--- + +## 7. ChatInfoView + +**Location:** [`ChatInfoView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt) + +Opened via the `info` callback when the user taps the toolbar title in a direct chat. Displayed in `ModalManager.end`. + +Preloads `apiContactInfo` (connection stats, server profile) and `apiGetContactCode` (verification code) before showing the modal. + +Key sections: contact profile, local alias, connection stats, shared media, disappearing messages preference, voice/call/file feature toggles, encryption verification, and contact deletion. + +--- + +## 8. GroupChatInfoView + +**Location:** [`group/GroupChatInfoView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt) + +Opened via the `info` callback for group chats. Displayed in `ModalManager.end`. + +Preloads group members (`setGroupMembers`) and group link (`apiGetGroupLink`). + +Key sections: group profile, group link, member list with roles, group preferences (disappearing messages, direct messages, full deletion, voice, files, SimpleX links, history), member admission, welcome message, reports view, and group deletion/leave. + +--- + +## 9. Source Files + +### `views/chat/` + +| File | Description | +|---|---| +| `ChatView.kt` | Main chat view, ChatLayout, ChatItemsList, ChatInfoToolbar | +| `ChatInfoView.kt` | Contact info modal | +| `ChatItemInfoView.kt` | Individual message delivery/read info | +| `ChatItemsLoader.kt` | Pagination and message loading logic | +| `ChatItemsMerger.kt` | MergedItems grouping of consecutive events | +| `CommandsMenuView.kt` | Bot `/command` menu popup | +| `ComposeContextContactRequestActionsView.kt` | Contact request action buttons in compose area | +| `ComposeContextGroupDirectInvitationActionsView.kt` | Group direct invitation compose actions | +| `ComposeContextPendingMemberActionsView.kt` | Pending member compose actions | +| `ComposeContextProfilePickerView.kt` | Profile picker in compose context | +| `ComposeFileView.kt` | File attachment preview in compose | +| `ComposeImageView.kt` | Image/video attachment preview in compose | +| `ComposeView.kt` | Main compose area (ComposeState, send logic) | +| `ComposeVoiceView.kt` | Voice recording preview in compose | +| `ContactPreferences.kt` | Per-contact feature preferences | +| `ContextItemView.kt` | Reply/edit/forward context indicator | +| `ScanCodeView.kt` | QR code scanner | +| `SelectableChatItemToolbars.kt` | Multi-select toolbar (delete, forward, moderate) | +| `SendMsgView.kt` | Text input field, send button, voice record button | +| `VerifyCodeView.kt` | Contact/member encryption verification | + +### `views/chat/item/` + +| File | Description | +|---|---| +| `ChatItemView.kt` | Message type routing, context menu, reactions | +| `CIBrokenComposableView.kt` | Fallback for rendering errors | +| `CICallItemView.kt` | Call event display (incoming/outgoing/missed) | +| `CIChatFeatureView.kt` | Chat feature change event | +| `CIEventView.kt` | Generic event display (group/direct/connection) | +| `CIFeaturePreferenceView.kt` | Feature preference change event | +| `CIFileView.kt` | File message (download/upload progress) | +| `CIGroupInvitationView.kt` | Group invitation card | +| `CIImageView.kt` | Image message (thumbnail + fullscreen) | +| `CIInvalidJSONView.kt` | Invalid JSON fallback display | +| `CIMemberCreatedContactView.kt` | Member-created contact event | +| `CIMetaView.kt` | Message metadata (time, status indicators) | +| `CIRcvDecryptionError.kt` | Decryption error display | +| `CIVideoView.kt` | Video message (thumbnail + player) | +| `CIVoiceView.kt` | Voice message (waveform + player) | +| `DeletedItemView.kt` | Deleted message placeholder | +| `EmojiItemView.kt` | Large emoji-only message | +| `FramedItemView.kt` | Message bubble frame (quoted item, text, media) | +| `ImageFullScreenView.kt` | Fullscreen image gallery | +| `IntegrityErrorItemView.kt` | Message integrity error | +| `MarkedDeletedItemView.kt` | Marked-as-deleted / moderated message | +| `TextItemView.kt` | Plain text message with markdown | + +### `views/chat/group/` + +| File | Description | +|---|---| +| `AddGroupMembersView.kt` | Add members to group | +| `GroupChatInfoView.kt` | Group info and management | +| `GroupLinkView.kt` | Group link display and management | +| `GroupMemberInfoView.kt` | Individual member info | +| `GroupMembersToolbar.kt` | Members toolbar in group info | +| `GroupMentions.kt` | @mention autocomplete | +| `GroupPreferences.kt` | Group feature preferences | +| `GroupProfileView.kt` | Group profile editor | +| `GroupReportsView.kt` | Group reports list view | +| `MemberAdmission.kt` | Member admission settings | +| `MemberSupportChatView.kt` | Member support chat (scoped context) | +| `MemberSupportView.kt` | Support chat list for moderators | +| `WelcomeMessageView.kt` | Group welcome message editor | diff --git a/apps/multiplatform/spec/client/compose.md b/apps/multiplatform/spec/client/compose.md new file mode 100644 index 0000000000..241dcf667b --- /dev/null +++ b/apps/multiplatform/spec/client/compose.md @@ -0,0 +1,399 @@ +# Message Composition Specification + +Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt`, `SendMsgView.kt` + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [ComposeState Data Class](#2-composestate-data-class) +3. [ComposePreview Sealed Class](#3-composepreview-sealed-class) +4. [ComposeContextItem Sealed Class](#4-composecontextitem-sealed-class) +5. [SendMsgView](#5-sendmsgview) +6. [Attachment Handling](#6-attachment-handling) +7. [Draft Persistence](#7-draft-persistence) +8. [Source Files](#8-source-files) + +--- + +## Executive Summary + +Message composition in SimpleX Chat is managed by `ComposeView` (line ~345 in `ComposeView.kt`) backed by the serializable `ComposeState` data class. The compose area supports text input, link previews, media/file/voice attachments, reply/edit/forward contexts, live (streaming) messages, member @mentions, message reports, and timed (disappearing) messages. The `SendMsgView` composable (in `SendMsgView.kt`) provides the text field and action buttons. Draft state persists across chat switches when the privacy preference is enabled. + +--- + + + +## 1. Overview + +``` +ComposeView +|-- contextItemView() +| |-- ContextItemView (QuotedItem) [reply indicator] +| |-- ContextItemView (EditingItem) [edit indicator] +| |-- ContextItemView (ForwardingItems) [forward indicator] +| +-- ContextItemView (ReportedItem) [report indicator] +|-- ReportReasonView [report reason header] +|-- MsgNotAllowedView [disabled send reason] +|-- previewView() +| |-- ComposeLinkView [link preview card] +| |-- ComposeImageView [media thumbnails] +| |-- ComposeVoiceView [voice recording waveform] +| +-- ComposeFileView [file name display] +|-- AttachmentAndCommandsButtons +| |-- CommandsButton [bot commands "//"] +| +-- AttachmentButton [paperclip icon] ++-- SendMsgView + |-- PlatformTextField [multiline text input] + |-- DeleteTextButton [clear text, shown on long text] + |-- SendMsgButton [arrow/check icon] + |-- RecordVoiceView [microphone + hold-to-record] + |-- StartLiveMessageButton [bolt icon] + |-- CancelLiveMessageButton [cancel live] + +-- TimedMessageDropdown [disappearing message timer] +``` + +--- + + + +## 2. ComposeState Data Class + +**Location:** [`ComposeView.kt#L98`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L98) + +```kotlin +@Serializable +data class ComposeState( + val message: ComposeMessage = ComposeMessage(), + val parsedMessage: List = emptyList(), + val liveMessage: LiveMessage? = null, + val preview: ComposePreview = ComposePreview.NoPreview, + val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem, + val inProgress: Boolean = false, + val progressByTimeout: Boolean = false, + val useLinkPreviews: Boolean, + val mentions: MentionedMembers = emptyMap() +) +``` + +### Fields + +| Field | Type | Description | +|---|---|---| +| `message` | `ComposeMessage` | Current text and cursor selection (`TextRange`) | +| `parsedMessage` | `List` | Markdown-parsed representation of message text | +| `liveMessage` | `LiveMessage?` | Active live (streaming) message state | +| `preview` | `ComposePreview` | Attachment preview (link, media, voice, file) | +| `contextItem` | `ComposeContextItem` | Reply/edit/forward/report context | +| `inProgress` | `Boolean` | Send operation in flight | +| `progressByTimeout` | `Boolean` | Show spinner after 1-second send delay | +| `useLinkPreviews` | `Boolean` | Link preview feature flag | +| `mentions` | `MentionedMembers` | Map of mention display name to `CIMention` | + +### Computed Properties + +| Property | Type | Description | +|---|---|---| +| `editing` | `Boolean` | True when `contextItem` is `EditingItem` | +| `forwarding` | `Boolean` | True when `contextItem` is `ForwardingItems` | +| `reporting` | `Boolean` | True when `contextItem` is `ReportedItem` | +| `sendEnabled` | `() -> Boolean` | True when there is content to send and not in progress | +| `linkPreviewAllowed` | `Boolean` | True when no media/voice/file preview is active | +| `linkPreview` | `LinkPreview?` | Extracts link preview from `CLinkPreview` | +| `attachmentDisabled` | `Boolean` | True when editing, forwarding, live, in-progress, or reporting | +| `attachmentPreview` | `Boolean` | True when a file or media preview is showing | +| `empty` | `Boolean` | True when no text, no preview, and no context item | +| `whitespaceOnly` | `Boolean` | True when message text contains only whitespace | +| `placeholder` | `String` | Input placeholder text (report reason text or default) | +| `memberMentions` | `Map` | Extracted member ID map for API calls | + +### ComposeMessage + +```kotlin +@Serializable +data class ComposeMessage( + val text: String = "", + val selection: TextRange = TextRange.Zero +) +``` + +### LiveMessage + +```kotlin +@Serializable +data class LiveMessage( + val chatItem: ChatItem, + val typedMsg: String, + val sentMsg: String, + val sent: Boolean +) +``` + +Tracks a live (streaming) message: the associated `ChatItem`, the currently typed text, the last sent text, and whether the initial send has occurred. + +### Serialization + +`ComposeState` is fully `@Serializable` with a custom `Saver` (line ~214) that uses `json.encodeToString`/`decodeFromString` for `rememberSaveable` persistence across configuration changes. + +--- + + + +## 3. ComposePreview Sealed Class + +**Location:** [`ComposeView.kt#L52`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L52) + +```kotlin +sealed class ComposePreview { + object NoPreview : ComposePreview() + class CLinkPreview(val linkPreview: LinkPreview?) : ComposePreview() + class MediaPreview(val images: List, val content: List) : ComposePreview() + data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean) : ComposePreview() + class FilePreview(val fileName: String, val uri: URI) : ComposePreview() +} +``` + +| Variant | Fields | View | +|---|---|---| +| `NoPreview` | -- | Nothing shown | +| `CLinkPreview` | `linkPreview: LinkPreview?` (null = loading) | `ComposeLinkView`: title, description, image thumbnail, cancel button | +| `MediaPreview` | `images: List` (base64 thumbnails), `content: List` | `ComposeImageView`: horizontal thumbnail strip, cancel button | +| `VoicePreview` | `voice: String` (file path), `durationMs: Int`, `finished: Boolean` | `ComposeVoiceView`: waveform visualization, duration, play/pause | +| `FilePreview` | `fileName: String`, `uri: URI` | `ComposeFileView`: file icon, file name, cancel button | + +### UploadContent + +Used within `MediaPreview` to track the source type: + +- `SimpleImage(uri: URI)` -- still image +- `AnimatedImage(uri: URI)` -- GIF or animated WebP +- `Video(uri: URI, duration: Int)` -- video with duration in seconds + +--- + +## 4. ComposeContextItem Sealed Class + +**Location:** [`ComposeView.kt#L61`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L61) + +```kotlin +sealed class ComposeContextItem { + object NoContextItem : ComposeContextItem() + class QuotedItem(val chatItem: ChatItem) : ComposeContextItem() + class EditingItem(val chatItem: ChatItem) : ComposeContextItem() + class ForwardingItems(val chatItems: List, val fromChatInfo: ChatInfo) : ComposeContextItem() + class ReportedItem(val chatItem: ChatItem, val reason: ReportReason) : ComposeContextItem() +} +``` + +| Variant | Trigger | Compose Behavior | +|---|---|---| +| `NoContextItem` | Default state | Normal message composition | +| `QuotedItem` | Swipe-to-reply or reply menu action | Shows quoted message indicator; sends with `quoted` parameter | +| `EditingItem` | Edit menu action | Populates text field with existing message; send button becomes checkmark; calls `apiUpdateChatItem` | +| `ForwardingItems` | Forward action from another chat | Shows forwarded items indicator; calls `apiForwardChatItems`; can include optional text message | +| `ReportedItem` | Report menu action | Shows report indicator with reason; placeholder changes to reason text; calls `apiReportMessage` | + +### Context Item View + +`contextItemView()` (line ~1098 in `ComposeView.kt`) renders the active context as a dismissible bar above the text input: + +- Icon: reply (ic_reply), edit (ic_edit_filled), forward (ic_forward), report (ic_flag) +- Content: quoted message preview text with sender name +- Close button: resets `contextItem` to `NoContextItem` (or `clearState()` for editing) + +--- + + + +## 5. SendMsgView + +**Location:** [`SendMsgView.kt#L36`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt#L36) + +```kotlin +fun SendMsgView( + composeState: MutableState, + showVoiceRecordIcon: Boolean, + recState: MutableState, + isDirectChat: Boolean, + liveMessageAlertShown: SharedPreference, + sendMsgEnabled: Boolean, + userCantSendReason: Pair?, + sendButtonEnabled: Boolean, + sendToConnect: (() -> Unit)?, + hideSendButton: Boolean, + nextConnect: Boolean, + needToAllowVoiceToContact: Boolean, + allowedVoiceByPrefs: Boolean, + sendButtonColor: Color, + allowVoiceToContact: () -> Unit, + timedMessageAllowed: Boolean, + customDisappearingMessageTimePref: SharedPreference?, + placeholder: String, + sendMessage: (Int?) -> Unit, + sendLiveMessage: (suspend () -> Unit)?, + updateLiveMessage: (suspend () -> Unit)?, + cancelLiveMessage: (() -> Unit)?, + editPrevMessage: () -> Unit, + onFilesPasted: (List) -> Unit, + onMessageChange: (ComposeMessage) -> Unit, + textStyle: MutableState, + focusRequester: FocusRequester? +) +``` + +### Layout + +The view is a `Box` containing: + +1. **PlatformTextField:** Multiline text input (platform-specific `expect`). Handles text changes via `onMessageChange`, up-arrow to `editPrevMessage`, file paste via `onFilesPasted`, and Enter to send. +2. **DeleteTextButton:** Shown when text is long; clears the field. +3. **Action area** (bottom-right, stacked): + - **Progress indicator:** Shown when `progressByTimeout` is true. + - **Report confirm button:** Checkmark icon when context is `ReportedItem`. + - **Voice record button:** Shown when message is empty, not editing/forwarding, no preview active. + - `RecordVoiceView`: Hold-to-record with waveform display. + - `DisallowedVoiceButton`: Shown when voice is disabled by preferences. + - `VoiceButtonWithoutPermissionByPlatform`: Shown when microphone permission is not granted. + - **Live message button:** Bolt icon, starts streaming message (calls `sendLiveMessage`). + - **Send button:** Arrow icon (new message) or checkmark (editing/live). Long-press opens dropdown: + - "Send live message" option + - Timed message options (1min, 5min, 1hr, 8hr, 1day, 1week, 1month, custom) + +### RecordingState + +```kotlin +sealed class RecordingState { + object NotStarted : RecordingState() + class Started(val filePath: String, val progressMs: Int) : RecordingState() + class Finished(val filePath: String, val durationMs: Int) : RecordingState() +} +``` + +Voice recording of 300ms or less is auto-cancelled. + +### Disabled State + +When `sendMsgEnabled` is false (e.g., contact not ready, group permissions), an overlay covers the text field. If `userCantSendReason` is provided, tapping the overlay shows an alert explaining why sending is disabled. + +--- + +## 6. Attachment Handling + + + +### Attachment Selection + +The `AttachmentSelection` composable (line ~263 in `ComposeView.kt`) is an `expect` function with platform-specific implementations: + +**Android:** +- Camera launcher (image capture) +- Gallery launcher (image/video picker, multi-select) +- File picker (any file type) + +**Desktop:** +- File chooser dialog (filters for images or all files) + +### ChooseAttachmentView + +Bottom sheet (`ModalBottomSheetLayout`) presenting attachment type options: + +| Option | Result | +|---|---| +| Camera (Android) | Launches camera intent; result processed as `SimpleImage` | +| Gallery | Launches media picker; results processed via `processPickedMedia` | +| File | Launches file picker; result processed via `processPickedFile` | + +### File Processing + +**`processPickedFile`** (line ~281): +1. Checks file size against `maxFileSize` (XFTP limit). +2. Extracts file name from URI. +3. Sets `ComposePreview.FilePreview` on compose state. + +**`processPickedMedia`** (line ~300): +1. For each URI, determines type (image, animated image, video). +2. Images: Gets bitmap, creates `SimpleImage` or `AnimatedImage` upload content. +3. Videos: Extracts thumbnail and duration, creates `Video` upload content. +4. Generates base64 preview thumbnails (max 14KB). +5. Sets `ComposePreview.MediaPreview` with thumbnails and content list. + +**`onFilesAttached`** (line ~270): +Groups dropped/pasted files into images and non-images; routes to `processPickedMedia` or `processPickedFile`. + +### Send Flow + +On send (line ~603, `sendMessageAsync`): + +1. **Forwarding:** Calls `apiForwardChatItems`, then optionally sends a text message quoting the last forwarded item. +2. **Editing:** Calls `apiUpdateChatItem` with updated `MsgContent`. +3. **Reporting:** Calls `apiReportMessage` with reason and text. +4. **New message:** Iterates over `msgs` (one per media item or single for text/file/voice): + - Saves file to app storage (or remote host). + - For voice: encrypts if `privacyEncryptLocalFiles` is enabled. + - Calls `apiSendMessages` or `apiCreateChatItems` (local notes). +5. On failure of the last message, restores compose state for retry. + +### Link Preview + +When `privacyLinkPreviews` is enabled and the message contains a URL: + +1. `showLinkPreview` extracts first non-SimpleX, non-cancelled link from parsed markdown. +2. Sets `ComposePreview.CLinkPreview(null)` (loading state). +3. After 1.5s debounce, calls `getLinkPreview(url)`. +4. On success, updates to `CLinkPreview(linkPreview)`. +5. Cancel button adds the URL to `cancelledLinks` set. + +--- + +## 7. Draft Persistence + +**Location:** [`ComposeView.kt#L1230`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L1230) (`KeyChangeEffect(chatModel.chatId.value)`) + +Controlled by the `privacySaveLastDraft` preference. + +### Save Behavior + +When the user navigates away from a chat (`chatModel.chatId.value` changes): + +| Compose State | Action | +|---|---| +| Live message active (text present or already sent) | Sends the live message immediately, clears draft | +| In progress | Clears in-progress flag, clears previous draft | +| Non-empty (text, preview, or context) | If `saveLastDraft` is true: saves `composeState.value` to `chatModel.draft.value` and `chatModel.draftChatId.value` | +| Empty but draft exists for current chat | Restores draft from `chatModel.draft` | +| Empty, no draft | Clears previous draft, deletes unused files | + +### Restore Behavior + +When entering a chat (line ~132 in `ChatView.kt`): + +1. Checks if `chatModel.draftChatId.value` matches the chat ID. +2. If match and draft is not null (and not a cross-chat forward), initializes `composeState` from the draft. +3. Otherwise, creates a fresh `ComposeState`. + +### Desktop-specific + +On desktop, a `DisposableEffect` (line ~1256) saves the draft on dispose when forwarding content, since the `KeyChangeEffect` mechanism is Android-specific. + +### Draft Display in Chat List + +When a draft exists for a chat, `ChatPreviewView` shows a pencil icon with the draft text instead of the last message preview. + +--- + +## 8. Source Files + +| File | Description | +|---|---| +| `ComposeView.kt` | ComposeState, ComposePreview, ComposeContextItem, ComposeView composable, send logic, link preview, draft persistence | +| `SendMsgView.kt` | Text input field, send/voice/live/timed buttons, recording state | +| `ComposeFileView.kt` | File attachment preview (name, cancel) | +| `ComposeImageView.kt` | Media attachment preview (thumbnails, cancel) | +| `ComposeVoiceView.kt` | Voice recording preview (waveform, duration, play) | +| `ContextItemView.kt` | Reply/edit/forward/report context bar | +| `ComposeContextContactRequestActionsView.kt` | Contact request action buttons in compose area | +| `ComposeContextGroupDirectInvitationActionsView.kt` | Group direct invitation compose actions | +| `ComposeContextPendingMemberActionsView.kt` | Pending member compose actions | +| `ComposeContextProfilePickerView.kt` | Profile picker in compose context | +| `SelectableChatItemToolbars.kt` | Multi-select mode toolbar (delete, forward, moderate) | diff --git a/apps/multiplatform/spec/client/navigation.md b/apps/multiplatform/spec/client/navigation.md new file mode 100644 index 0000000000..c9939ea3c0 --- /dev/null +++ b/apps/multiplatform/spec/client/navigation.md @@ -0,0 +1,379 @@ +# Navigation Specification + +Source: `common/src/commonMain/kotlin/chat/simplex/common/App.kt` (470 lines) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [AppScreen Composable](#2-appscreen-composable) +3. [MainScreen](#3-mainscreen) +4. [Android Layout](#4-android-layout) +5. [Desktop Layout](#5-desktop-layout) +6. [ModalManager](#6-modalmanager) +7. [Authentication Gate](#7-authentication-gate) +8. [Onboarding Flow](#8-onboarding-flow) +9. [Source Files](#9-source-files) + +--- + +## Executive Summary + +SimpleX Chat navigation is a platform-adaptive system implemented in `App.kt`. The root `AppScreen` composable applies theming and safe-area insets, delegating to `MainScreen` which acts as a state machine routing between onboarding, authentication, database error, and the main chat interface. Android uses a 2-column sliding layout (`AndroidScreen`), while desktop uses a fixed 3-column layout (`DesktopScreen`). Modal presentation is managed by `ModalManager`, which provides named zones (start, center, end, fullscreen) for layered content. Authentication is gated by `AppLock`, and onboarding follows a linear `OnboardingStage` enum. + +--- + +## 1. Overview + +``` +AppScreen (line 46) ++-- SimpleXTheme + +-- Surface + +-- MainScreen (line 82) + |-- [Migration in progress] -> DefaultProgressView + |-- [Database opening] -> DefaultProgressView + |-- [Database error] -> DatabaseErrorView + |-- [Encryption check pending] -> SplashView + |-- [Onboarding incomplete] -> AnimatedContent { OnboardingStage views } + |-- [Onboarding complete] + | |-- [Android] + | | +-- AndroidWrapInCallLayout + | | +-- AndroidScreen (line 293) + | | |-- StartPartOfScreen (ChatListView) + | | +-- ChatView (slide-in panel) + | +-- [Desktop] + | +-- DesktopScreen (line 406) + | |-- StartPartOfScreen + UserPicker (left column) + | |-- ModalManager.start (overlay on left) + | |-- CenterPartOfScreen / ChatView (center column) + | +-- ModalManager.end (right column) + |-- [Unauthorized] -> AuthView / SplashView / PasscodeView + |-- [Active call] -> ActiveCallView (desktop) / startCallActivity (Android) + +-- [Incoming call] -> IncomingCallAlertView +``` + +--- + + + +## 2. AppScreen Composable + +**Location:** [`App.kt#L47`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L47) + +```kotlin +@Composable +fun AppScreen() +``` + +### Responsibilities + +1. **Theme application:** Wraps content in `SimpleXTheme` with `Surface` using `MaterialTheme.colors.background`. +2. **Window insets:** Computes safe padding for landscape mode, accounting for display cutouts on both sides. Uses `WindowInsets.safeDrawing` and `WindowInsets.displayCutout` to calculate symmetric padding. +3. **Fullscreen gallery overlay:** When `chatModel.fullscreenGalleryVisible` is true, draws a black rectangle behind content extending into the cutout areas to provide an immersive gallery background. +4. **Delegates to `MainScreen()`.** + +--- + + + +## 3. MainScreen + +**Location:** [`App.kt#L84`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L84) + +```kotlin +@Composable +fun MainScreen() +``` + +### State Machine + +`MainScreen` evaluates a series of conditions in priority order: + +| Priority | Condition | View | +|---|---|---| +| 1 | `onboarding == Step1_SimpleXInfo && migrationState != null` | `SimpleXInfo` (migration in progress) | +| 2 | `dbMigrationInProgress` | `DefaultProgressView("Database migration...")` | +| 3 | `chatDbStatus == null && showInitializationView` | `DefaultProgressView("Opening database...")` | +| 4 | `showChatDatabaseError` | `DatabaseErrorView` | +| 5 | `chatDbEncrypted == null \|\| localUserCreated == null` | `SplashView` | +| 6 | `onboarding == OnboardingComplete` | Platform-specific main screen | +| 7 | Other onboarding stages | `AnimatedContent` with stage-specific views | + +### Onboarding Complete Branch (line ~156) + +When onboarding is complete: + +1. Shows "advertise lock" alert if conditions met (not shown before, LA not enabled, >3 chats, no active call). +2. Sets up clipboard listener. +3. Routes to `AndroidScreen` or `DesktopScreen` based on platform. + +### Overlay Layers (bottom of MainScreen) + +| Layer | Condition | Content | +|---|---|---| +| `ModalManager.fullscreen` | Android + migration/onboarding | Fullscreen modals | +| `SwitchingUsersView` | User switch in progress | Loading overlay | +| Auth gate | `userAuthorized != true` | `AuthView` or `SplashView` + passcode | +| Active call | `showCallView == true` | `ActiveCallView` (desktop) or call activity (Android) | +| One-time passcode | Always | `ModalManager.fullscreen.showOneTimePasscodeInView` | +| Privacy alerts | Always | `AlertManager.privacySensitive` | +| Incoming call | `activeCallInvitation != null` | `IncomingCallAlertView` | +| Shared alerts | Always | `AlertManager.shared` | + +--- + + + +## 4. Android Layout + +**Location:** [`App.kt#L296`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L296) + +```kotlin +@Composable +fun AndroidScreen(userPickerState: MutableStateFlow) +``` + +### 2-Column Slide Animation + +Uses `BoxWithConstraints` to get `maxWidth`, then two `Box` containers: + +1. **Left panel (StartPartOfScreen):** Chat list, positioned at `translationX = -offset`. +2. **Right panel (ChatView):** Chat view, positioned at `translationX = maxWidth - offset`. + +The `offset` is an `Animatable`: +- `0f` when no chat is selected (chat list visible). +- `maxWidth.value` when a chat is open (chat view visible). + +### Animation Flow + +1. `snapshotFlow { chatModel.chatId.value }` detects chat ID changes. +2. When `chatId` becomes null, `onComposed(null)` animates offset to 0. +3. When `ChatView` finishes composing (calls `onComposed(chatId)`), offset animates to `maxWidth`. +4. Animation uses `chatListAnimationSpec()` (standard spring or tween). + +### Display Cutout Handling + +If the device has a display cutout on horizontal sides (detected via `WindowInsets.displayCutout`), the panels are clipped with `RectangleShape` to prevent the chat list from showing through during transition. + +### Call Layout Wrapper + +`AndroidWrapInCallLayout` (line ~279) adds a 40dp top padding when an active call is in progress (not in `WaitCapabilities` or `InvitationAccepted` state), with an `ActiveCallInteractiveArea` banner above. + +--- + + + +## 5. Desktop Layout + +**Location:** [`App.kt#L410`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L410) + +```kotlin +@Composable +fun DesktopScreen(userPickerState: MutableStateFlow) +``` + +### 3-Column Layout + +| Column | Width | Content | +|---|---|---| +| **Left** | `DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier` (fixed) | `StartPartOfScreen` (ChatListView) + `UserPicker` overlay | +| **Left overlay** | Same as left column | `ModalManager.start` modals + `SwitchingUsersView` | +| **Center** | `min = DEFAULT_MIN_CENTER_MODAL_WIDTH`, `weight = 1f` (flexible) | `CenterPartOfScreen` (ChatView or "no selected chat" placeholder, or `ModalManager.center`) | +| **Right** | `max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier` (flexible, 0 when empty) | `ModalManager.end` (ChatInfoView, GroupChatInfoView, ChatItemInfoView, etc.) | + +### Column Separators + +- `VerticalDivider` between left and center columns (always visible). +- `VerticalDivider` between center and right columns (visible when `ModalManager.end.hasModalsOpen()`). + +### Click-to-Dismiss Overlay + +When the UserPicker is visible or a start modal is open (but no center modal), a full-size clickable overlay covers the center+right area (line ~428). Clicking it closes start modals and hides the UserPicker. + +### CenterPartOfScreen + +**Location:** [`App.kt#L373`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L373) + +- When `chatId` is null and no center modals: shows "No selected chat" placeholder. +- When `chatId` is null and center modals open: shows `ModalManager.center`. +- When `chatId` is set: shows `ChatView`. +- Automatically closes center modals when a chat is selected. + +### StartPartOfScreen + +**Location:** [`App.kt#L352`](../../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L352) + +Routes between: +- `SetDeliveryReceiptsView` (if `chatModel.setDeliveryReceipts` is true) +- `ChatListView` (normal operation) +- `ShareListView` (when `chatModel.sharedContent` is non-null, i.e., forwarding) + +--- + +## 6. ModalManager + +**Location:** `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt` (line 92) + +```kotlin +class ModalManager(private val placement: ModalPlacement?) +``` + +### Zones + +| Zone | Android Behavior | Desktop Behavior | +|---|---|---| +| `start` | Shared (same as all others) | Left column overlay, slides from start | +| `center` | Shared | Center column overlay, replaces ChatView | +| `end` | Shared | Right column, slides from end | +| `fullscreen` | Shared | Fullscreen overlay | + +On Android, all four zones point to the same `shared` instance, meaning modals stack in a single overlay. On desktop, each zone is independent with its own `ModalPlacement`. + +```kotlin +companion object { + val start = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.START) + val center = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.CENTER) + val end = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.END) + val fullscreen = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.FULLSCREEN) +} +``` + +### Modal Stack + +Each `ModalManager` maintains a stack of `ModalViewHolder` objects with: +- `id: ModalViewId?` -- optional identifier for deduplication +- `animated: Boolean` -- whether to use enter/exit transitions +- `data: ModalData` -- scoped data for the modal +- `modal: @Composable ModalData.(close: () -> Unit) -> Unit` -- the modal content + +### Key Methods + +| Method | Description | +|---|---| +| `showModal` | Push a simple modal onto the stack | +| `showModalCloseable` | Push a modal with a close callback | +| `showCustomModal` | Push a modal with full control over `ModalView` wrapper | +| `closeModals` | Pop all modals from the stack | +| `closeModalsExceptFirst` | Pop all but the bottom modal | +| `hasModalsOpen()` | Check if any modals are on the stack | +| `showInView` | Render the current modal stack into the composable tree | + +### Usage Pattern + +| Action | Zone Used | +|---|---| +| Settings, New Chat, User Address | `ModalManager.start` | +| Onboarding conditions, What's New | `ModalManager.center` | +| ChatInfoView, GroupChatInfoView, ChatItemInfoView, GroupMemberInfoView | `ModalManager.end` | +| Passcode entry, Call view, Migration | `ModalManager.fullscreen` | + +--- + + + +## 7. Authentication Gate + +**Location:** [`AppLock.kt#L17`](../../common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt#L17) + +```kotlin +object AppLock { + val userAuthorized = mutableStateOf(null) + val enteredBackground = mutableStateOf(null) + val laFailed = mutableStateOf(false) +} +``` + +### State + +| Field | Type | Description | +|---|---|---| +| `userAuthorized` | `MutableState` | `null` = not yet determined, `true` = authenticated, `false` = locked | +| `enteredBackground` | `MutableState` | Timestamp when app entered background (for lock delay) | +| `laFailed` | `MutableState` | True if last authentication attempt failed | + +### Authentication Flow + +1. **MainScreen** checks `unauthorized` (derived: `userAuthorized.value != true`) at line ~135. +2. If unauthorized and not in an active call: + - Launches `AppLock.runAuthenticate()` which triggers platform-specific biometric/passcode prompt. + - On Android with system auth finishing during activity destruction, authentication is skipped. +3. If `performLA` preference is set and `laFailed` is true: shows `AuthView` with "Unlock" button. +4. If `performLA` is set and `laFailed` is false: shows `SplashView` with passcode overlay. + +### Lock Delay + +The `laLockDelay` preference controls how long after backgrounding the app requires re-authentication. When `laLockDelay == 0`, screen rotation triggers a 3-second grace period (line ~270) to prevent unnecessary re-auth. + +### Lock Modes + +- `LAMode.SYSTEM`: Uses Android biometric/system lock screen. +- `LAMode.PASSCODE`: Uses in-app passcode (`SetAppPasscodeView`). + +### First-Time Lock Notice + +`showLANotice` (line ~33 in `AppLock.kt`) prompts users to enable SimpleX Lock when they have more than 3 chats, have not yet been shown the notice, and have not enabled lock. On Android, it offers a choice between system auth and passcode. + +--- + +## 8. Onboarding Flow + +**Location:** `common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt` (line 3) + +```kotlin +enum class OnboardingStage { + Step1_SimpleXInfo, + Step2_CreateProfile, + LinkAMobile, + Step2_5_SetupDatabasePassphrase, + Step3_ChooseServerOperators, + Step3_CreateSimpleXAddress, + Step4_SetNotificationsMode, + OnboardingComplete +} +``` + +### Stage Progression + +| Stage | View | Next Stage | +|---|---|---| +| `Step1_SimpleXInfo` | `SimpleXInfo` -- app introduction, privacy features | `Step2_CreateProfile` or `LinkAMobile` (desktop) | +| `Step2_CreateProfile` | `CreateFirstProfile` -- display name, optional image | `Step2_5_SetupDatabasePassphrase` or `Step3_ChooseServerOperators` | +| `LinkAMobile` | `LinkAMobile` -- desktop linking to mobile device | `Step2_CreateProfile` | +| `Step2_5_SetupDatabasePassphrase` | `SetupDatabasePassphrase` -- optional DB encryption | `Step3_ChooseServerOperators` | +| `Step3_ChooseServerOperators` | `OnboardingConditionsView` -- server operator selection, T&C | `Step3_CreateSimpleXAddress` or `Step4_SetNotificationsMode` | +| `Step3_CreateSimpleXAddress` | `SetNotificationsMode` (legacy backcompat) | `Step4_SetNotificationsMode` | +| `Step4_SetNotificationsMode` | `SetNotificationsMode` -- notification permission setup | `OnboardingComplete` | +| `OnboardingComplete` | Main app screen | -- | + +### Animated Transitions + +Onboarding uses `AnimatedContent` with directional transitions: +- Forward: `fromEndToStartTransition` (slide left). +- Backward: `fromStartToEndTransition` (slide right). + +The stage value is stored in `appPrefs.onboardingStage` and persisted across app restarts. + +--- + +## 9. Source Files + +| File | Description | +|---|---| +| `App.kt` | AppScreen, MainScreen, AndroidScreen, DesktopScreen, StartPartOfScreen, CenterPartOfScreen, EndPartOfScreen | +| `AppLock.kt` | AppLock object, authentication state, lock notice, LA mode selection | +| `views/helpers/ModalView.kt` | ModalManager class, ModalPlacement enum, modal stack management | +| `views/onboarding/OnboardingView.kt` | OnboardingStage enum | +| `views/onboarding/SimpleXInfo.kt` | Step 1: App introduction | +| `views/WelcomeView.kt` | Step 2: Profile creation (CreateFirstProfile) | +| `views/onboarding/LinkAMobileView.kt` | Desktop: Link a mobile device | +| `views/onboarding/SetupDatabasePassphrase.kt` | Step 2.5: Database passphrase | +| `views/onboarding/ChooseServerOperators.kt` | Step 3: Server operators and conditions | +| `views/onboarding/SetNotificationsMode.kt` | Step 4: Notification setup | +| `views/chatlist/ChatListView.kt` | Chat list (StartPartOfScreen content) | +| `views/chatlist/UserPicker.kt` | User switching panel | +| `views/chat/ChatView.kt` | Chat view (CenterPartOfScreen content) | +| `views/database/DatabaseErrorView.kt` | Database error recovery | +| `views/SplashView.kt` | Splash / loading screen | +| `views/call/CallView.kt` | In-call fullscreen view (ActiveCallView) | +| `views/localauth/PasswordEntry.kt` | Column divider utility (contains VerticalDivider) | diff --git a/apps/multiplatform/spec/database.md b/apps/multiplatform/spec/database.md new file mode 100644 index 0000000000..f6ecedb721 --- /dev/null +++ b/apps/multiplatform/spec/database.md @@ -0,0 +1,393 @@ +# Database & Storage + +## Table of Contents + +1. [Overview](#1-overview) +2. [Database Files & Paths](#2-database-files--paths) +3. [Haskell Store Modules](#3-haskell-store-modules) +4. [Migrations](#4-migrations) +5. [Database Encryption](#5-database-encryption) +6. [File Storage](#6-file-storage) +7. [Export & Import](#7-export--import) +8. [Source Files](#8-source-files) + +--- + +## 1. Overview + +SimpleX Chat uses **two SQLite databases** managed entirely by the Haskell core. Kotlin code **never reads or writes the databases directly** -- all data access goes through the JNI command/response protocol defined in [SimpleXAPI.kt](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt). + +The two databases are: + +| Database | Suffix | Contents | +|----------|--------|----------| +| Chat database | `_chat.db` | Users, contacts, groups, messages, files metadata, settings | +| Agent database | `_agent.db` | SMP/XFTP agent state: connections, queues, encryption keys, delivery tracking | + +Both databases are created and migrated by the `chatMigrateInit` JNI function. The Kotlin layer handles: +- Providing the correct file path prefix (`dbAbsolutePrefixPath`) +- Providing the encryption key +- Interpreting migration results (`DBMigrationResult`) +- Exposing API functions that proxy to Haskell store operations + +--- + +## 2. Database Files & Paths + +### Expect Declarations + +The common module declares platform-dependent paths as `expect` values in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt): + +```kotlin +expect val dataDir: File // L18 +expect val tmpDir: File // L19 +expect val filesDir: File // L20 +expect val appFilesDir: File // L21 +expect val wallpapersDir: File // L22 +expect val coreTmpDir: File // L23 +expect val dbAbsolutePrefixPath: String // L24 +expect val preferencesDir: File // L25 +expect val preferencesTmpDir: File // L26 + +expect val chatDatabaseFileName: String // L28 +expect val agentDatabaseFileName: String // L29 + +expect val databaseExportDir: File // L35 +expect val remoteHostsDir: File // L37 +``` + +### Android Actual Values + +From [Files.android.kt](../common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt): + +| Variable | Value | Notes | +|----------|-------|-------| +| `dataDir` | `androidAppContext.dataDir` | `/data/data//` | +| `tmpDir` | `getDir("temp", MODE_PRIVATE)` | Private temp directory | +| `filesDir` | `dataDir/files` | Parent for all file storage | +| `appFilesDir` | `filesDir/app_files` | User-visible chat file attachments | +| `wallpapersDir` | `filesDir/assets/wallpapers` | Custom wallpaper images | +| `coreTmpDir` | `filesDir/temp_files` | Haskell core temp directory | +| `dbAbsolutePrefixPath` | `dataDir/files` | Prefix: core appends `_chat.db` / `_agent.db` | +| `chatDatabaseFileName` | `"files_chat.db"` | Full filename: `files_chat.db` | +| `agentDatabaseFileName` | `"files_agent.db"` | Full filename: `files_agent.db` | +| `databaseExportDir` | `androidAppContext.cacheDir` | Temp location for archive export | +| `remoteHostsDir` | `tmpDir/remote_hosts` | Remote host file staging | +| `preferencesDir` | `dataDir/shared_prefs` | Android SharedPreferences directory | + +### Desktop Actual Values + +From [Files.desktop.kt](../common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt): + +| Variable | Value | Notes | +|----------|-------|-------| +| `dataDir` | `desktopPlatform.dataPath` | XDG_DATA_HOME (Linux), AppData (Windows), Application Support (macOS) | +| `tmpDir` | `java.io.tmpdir/simplex` | System temp with `deleteOnExit` | +| `filesDir` | `dataDir/simplex_v1_files` | Flat file storage | +| `appFilesDir` | Same as `filesDir` | No subdirectory on desktop | +| `wallpapersDir` | `dataDir/simplex_v1_assets/wallpapers` | Custom wallpaper images | +| `coreTmpDir` | `dataDir/tmp` | Haskell core temp directory | +| `dbAbsolutePrefixPath` | `dataDir/simplex_v1` | Prefix: core appends `_chat.db` / `_agent.db` | +| `chatDatabaseFileName` | `"simplex_v1_chat.db"` | Full filename: `simplex_v1_chat.db` | +| `agentDatabaseFileName` | `"simplex_v1_agent.db"` | Full filename: `simplex_v1_agent.db` | +| `databaseExportDir` | Same as `tmpDir` | Temp location for archive export | +| `remoteHostsDir` | `dataDir/remote_hosts` | Remote host file staging | +| `preferencesDir` | `desktopPlatform.configPath` | Platform config directory | + +### Resulting Database Paths + +| Platform | Chat DB | Agent DB | +|----------|---------|----------| +| Android | `/data/data//files_chat.db` | `/data/data//files_agent.db` | +| Desktop (Linux) | `~/.local/share/simplex/simplex_v1_chat.db` | `~/.local/share/simplex/simplex_v1_agent.db` | +| Desktop (macOS) | `~/Library/Application Support/simplex/simplex_v1_chat.db` | ... | +| Desktop (Windows) | `%APPDATA%/simplex/simplex_v1_chat.db` | ... | + +--- + +## 3. Haskell Store Modules + +The Haskell core organizes database access into store modules. Kotlin code invokes these indirectly through `CC` commands. The store modules are: + +| Module | Path | Responsibilities | +|--------|------|-----------------| +| `Messages.hs` | `src/Simplex/Chat/Store/Messages.hs` | Message CRUD, chat items, reactions, delivery statuses, TTL cleanup | +| `Groups.hs` | `src/Simplex/Chat/Store/Groups.hs` | Group profiles, membership, roles, invitations, group links | +| `Direct.hs` | `src/Simplex/Chat/Store/Direct.hs` | Contact management, direct connections, contact requests | +| `Files.hs` | `src/Simplex/Chat/Store/Files.hs` | File transfer metadata, XFTP state, standalone files | +| `Profiles.hs` | `src/Simplex/Chat/Store/Profiles.hs` | User profiles, display names, address book | +| `Connections.hs` | `src/Simplex/Chat/Store/Connections.hs` | SMP agent connections, pending connections, server switches | + +All store operations execute within SQLite transactions managed by the Haskell core. The Kotlin layer has no direct knowledge of table schemas or SQL queries. + +--- + +## 4. Migrations + +### JNI Entry Point + +Database migration is triggered by the `chatMigrateInit` external function ([Core.kt#L25](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L25)): + +```kotlin +external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array +``` + +**Parameters:** +- `dbPath` -- the `dbAbsolutePrefixPath` (core appends `_chat.db` and `_agent.db`) +- `dbKey` -- encryption passphrase (empty string = unencrypted) +- `confirm` -- migration confirmation mode: `"error"`, `"yesUp"`, or `"yesUpDown"` + +**Returns:** `Array` where: +- `[0]` -- JSON string encoding a `DBMigrationResult` +- `[1]` -- `ChatCtrl` handle (Long) if migration succeeded + +### Migration Flow in `initChatController` + +The full initialization sequence is in [Core.kt#L62](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L62): + +1. Obtain the DB encryption key from `DatabaseUtils.useDatabaseKey()`. +2. Determine the confirmation mode (default: `YesUp`; developer mode with confirm upgrades: `Error`). +3. Call `chatMigrateInit(dbAbsolutePrefixPath, dbKey, "error")` -- first attempt with `Error` to detect pending migrations. +4. Parse the result as `DBMigrationResult`. +5. If the result is `ErrorMigration` with an `Upgrade` error and confirmation allows it, re-run `chatMigrateInit` with the appropriate confirmation (`"yesUp"`). +6. If `OK`, store the `ChatCtrl` handle, set `chatDbEncrypted`, and proceed to start the chat. +7. If not `OK`, handle special case: if the `newDatabaseInitialized` preference is not set AND the database was only partially initialized (single DB file exists), remove both files and retry once. + + + +### DBMigrationResult + +Defined in [DatabaseUtils.kt#L79](../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt#L79): + +```kotlin +sealed class DBMigrationResult { + object OK // Migration succeeded + object InvalidConfirmation // Invalid confirmation parameter + data class ErrorNotADatabase(val dbFile: String) // File exists but is not a valid database + data class ErrorMigration(val dbFile: String, // Migration error with details + val migrationError: MigrationError) + data class ErrorSQL(val dbFile: String, // SQL error during migration + val migrationSQLError: String) + object ErrorKeychain // Keychain/keystore error + data class Unknown(val json: String) // Unparseable response +} +``` + +### MigrationError + +```kotlin +sealed class MigrationError { + class Upgrade(val upMigrations: List) // Pending forward migrations + class Downgrade(val downMigrations: List) // Database is newer than app + class Error(val mtrError: MTRError) // Conflict or missing migrations +} +``` + +### MigrationConfirmation + +```kotlin +enum class MigrationConfirmation(val value: String) { + YesUp("yesUp"), // Auto-confirm forward migrations + YesUpDown("yesUpDown"), // Auto-confirm both directions (not used in UI) + Error("error") // Report errors without running migrations +} +``` + +--- + +## 5. Database Encryption + +### Encryption API + +Two API functions manage database encryption, both in [SimpleXAPI.kt](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt): + +| Function | Parameters | Description | Line | +|----------|-----------|-------------|------| +| `apiStorageEncryption` | `currentKey: String, newKey: String` | Change or set the database encryption passphrase | [L999](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L999) | +| `testStorageEncryption` | `key: String, ctrl: ChatCtrl?` | Test whether a given key can decrypt the database | [L1006](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1006) | + +Both delegate to the Haskell core via `CC.ApiStorageEncryption(DBEncryptionConfig)` and `CC.TestStorageEncryption(key)` respectively. + + + +`DBEncryptionConfig` ([SimpleXAPI.kt#L4166](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4166)): + +```kotlin +class DBEncryptionConfig(val currentKey: String, val newKey: String) +``` + +### Passphrase Storage -- CryptorInterface + +The `CryptorInterface` ([Cryptor.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt)) provides platform-specific key encryption for storing the DB passphrase at rest: + +```kotlin +interface CryptorInterface { + fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? + fun encryptText(text: String, alias: String): Pair + fun deleteKey(alias: String) +} + +expect val cryptor: CryptorInterface +``` + +### Android Implementation + +[Cryptor.android.kt](../common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt): + +- Uses **Android KeyStore** (`"AndroidKeyStore"` provider) +- Algorithm: **AES/GCM/NoPadding** (128-bit authentication tag) +- Keys are hardware-backed when available +- On decryption failure with a random initial passphrase, throws to prevent overwriting +- Shows user alerts for keychain errors + +```kotlin +internal class Cryptor: CryptorInterface { + private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + // AES-GCM encryption/decryption using AndroidKeyStore-managed keys +} +``` + +### Desktop Implementation + +[Cryptor.desktop.kt](../common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt): + +- **Placeholder/no-op implementation** -- data is returned as-is +- No actual encryption of the stored passphrase on desktop +- `decryptData` returns `String(data)` without decryption +- `encryptText` returns the raw bytes without encryption + +```kotlin +actual val cryptor: CryptorInterface = object : CryptorInterface { + override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? = String(data) + override fun encryptText(text: String, alias: String) = text.toByteArray() to text.toByteArray() + override fun deleteKey(alias: String) {} +} +``` + +### Passphrase Management + +`DatabaseUtils` ([DatabaseUtils.kt](../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt)) provides: + +- `ksDatabasePassword` -- encrypted passphrase stored in platform preferences (SharedPreferences on Android, file-based on desktop) +- `useDatabaseKey()` -- retrieves the passphrase, decrypting it via `CryptorInterface` +- `randomDatabasePassword()` -- generates a 32-byte random passphrase (Base64-encoded) for initial database creation + +The flow: +1. On first launch, `randomDatabasePassword()` generates a key. +2. `CryptorInterface.encryptText()` encrypts the key for storage. +3. The encrypted (data, IV) pair is saved to preferences via `ksDatabasePassword`. +4. On subsequent launches, `ksDatabasePassword.get()` retrieves the encrypted pair, and `CryptorInterface.decryptData()` recovers the plaintext key. +5. The key is passed to `chatMigrateInit` to open the encrypted SQLite databases. + +--- + +## 6. File Storage + +### Directory Layout + +Declared in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) with platform-specific implementations: + +| Directory | Variable | Android Path | Desktop Path | Purpose | +|-----------|----------|-------------|--------------|---------| +| App files | `appFilesDir` | `dataDir/files/app_files` | `dataDir/simplex_v1_files` | Chat file attachments (images, videos, documents) | +| Wallpapers | `wallpapersDir` | `dataDir/files/assets/wallpapers` | `dataDir/simplex_v1_assets/wallpapers` | Custom chat wallpaper images | +| Core temp | `coreTmpDir` | `dataDir/files/temp_files` | `dataDir/tmp` | Haskell core temporary files (in-progress transfers) | +| App temp | `tmpDir` | `getDir("temp", MODE_PRIVATE)` | `java.io.tmpdir/simplex` | Application-level temporary files | +| Remote hosts | `remoteHostsDir` | `tmpDir/remote_hosts` | `dataDir/remote_hosts` | Files staged for remote host sessions | +| DB export | `databaseExportDir` | `androidAppContext.cacheDir` | Same as `tmpDir` | Temporary storage for database archive ZIP | +| Preferences | `preferencesDir` | `dataDir/shared_prefs` | `desktopPlatform.configPath` | User preferences, theme YAML | +| Migration temp | `getMigrationTempFilesDirectory()` | `dataDir/migration_temp_files` | `dataDir/migration_temp_files` | Temporary files during database migration | + +### File Path Resolution + +Files referenced by chat items use `CryptoFile` (optional encryption metadata + relative path). Path resolution is handled by helper functions in [Files.kt](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt): + +- `getAppFilePath(fileName)` ([L81](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L81)) -- resolves to `appFilesDir/fileName` for local, or `remoteHostsDir//simplex_v1_files/fileName` for remote hosts +- `getWallpaperFilePath(fileName)` ([L91](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L91)) -- resolves wallpaper paths similarly +- `getLoadedFilePath(file)` ([L105](../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L105)) -- returns the full path if the file is downloaded and ready + +### Local File Encryption + +The `apiSetEncryptLocalFiles(enable)` command ([SimpleXAPI.kt#L967](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L967)) tells the Haskell core to encrypt files stored in `appFilesDir`. When enabled, files are written as `CryptoFile` with a random AES key and nonce. The JNI functions `chatEncryptFile` and `chatDecryptFile` ([Core.kt#L39-L40](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L39)) handle the actual crypto operations. + +--- + +## 7. Export & Import + +### API Functions + +| Function | CC Command | CR Response | Line | +|----------|-----------|-------------|------| +| `apiExportArchive(config)` | `CC.ApiExportArchive(config)` | `CR.ArchiveExported(archiveErrors)` | [SimpleXAPI.kt#L981](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L981) | +| `apiImportArchive(config)` | `CC.ApiImportArchive(config)` | `CR.ArchiveImported(archiveErrors)` | [SimpleXAPI.kt#L987](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L987) | +| `apiDeleteStorage()` | `CC.ApiDeleteStorage()` | `CR.CmdOk` | [SimpleXAPI.kt#L993](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L993) | + +### ArchiveConfig + +Defined at [SimpleXAPI.kt#L4162](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L4162): + +```kotlin +class ArchiveConfig( + val archivePath: String, // Full path to the ZIP archive + val disableCompression: Boolean?, // Skip compression for speed + val parentTempDirectory: String? // Temp directory for extraction +) +``` + +### Export Flow + +1. UI constructs an `ArchiveConfig` with a path under `databaseExportDir`. +2. Calls `apiExportArchive(config)` which sends `CC.ApiExportArchive` to the Haskell core. +3. The core creates a ZIP containing both `_chat.db` and `_agent.db` (and optionally files). +4. Returns `CR.ArchiveExported` with a list of `ArchiveError` (non-fatal issues during export). +5. UI offers the archive file for sharing/saving. + +### Import Flow + +1. User selects an archive file. +2. UI copies it to a temp location and constructs an `ArchiveConfig`. +3. Calls `apiImportArchive(config)` which sends `CC.ApiImportArchive` to the Haskell core. +4. The core extracts and replaces both databases. +5. Returns `CR.ArchiveImported` with a list of `ArchiveError` (non-fatal issues during import). +6. UI triggers re-initialization via `initChatController`. + + + +### ArchiveError + +Defined at [SimpleXAPI.kt#L7658](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7658): + +```kotlin +sealed class ArchiveError { + class ArchiveErrorImport(val importError: String) // General import error + class ArchiveErrorFile(val file: String, val fileError: String) // Per-file error +} +``` + +### Delete Storage + +`apiDeleteStorage()` removes both database files entirely. This is used during account deletion or database reset operations. After calling this, `initChatController` must be called to create fresh databases. + +--- + +## 8. Source Files + +| File | Purpose | Path | +|------|---------|------| +| SimpleXAPI.kt | API functions: encryption, export/import, storage commands | `common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | +| Core.kt | JNI externals (`chatMigrateInit`, `chatEncryptFile`, etc.), `initChatController` | `common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt` | +| Files.kt | Platform-expect file/directory path declarations | `common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt` | +| Files.android.kt | Android actual paths (dataDir, appFilesDir, etc.) | `common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt` | +| Files.desktop.kt | Desktop actual paths (XDG/AppData, etc.) | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt` | +| Cryptor.kt | Platform-expect encryption interface for passphrase storage | `common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt` | +| Cryptor.android.kt | Android: AES-GCM via AndroidKeyStore | `common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt` | +| Cryptor.desktop.kt | Desktop: placeholder (no-op) implementation | `common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt` | +| DatabaseUtils.kt | `DBMigrationResult`, `MigrationError`, `MigrationConfirmation`, passphrase helpers | `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt` | +| Messages.hs | Haskell store: message CRUD, reactions, delivery | `src/Simplex/Chat/Store/Messages.hs` | +| Groups.hs | Haskell store: groups, membership, roles | `src/Simplex/Chat/Store/Groups.hs` | +| Direct.hs | Haskell store: contacts, direct connections | `src/Simplex/Chat/Store/Direct.hs` | +| Files.hs | Haskell store: file transfer metadata | `src/Simplex/Chat/Store/Files.hs` | +| Profiles.hs | Haskell store: user profiles | `src/Simplex/Chat/Store/Profiles.hs` | +| Connections.hs | Haskell store: SMP agent connections | `src/Simplex/Chat/Store/Connections.hs` | + +All Kotlin paths are relative to `apps/multiplatform/`. All Haskell paths are relative to the repository root. diff --git a/apps/multiplatform/spec/impact.md b/apps/multiplatform/spec/impact.md new file mode 100644 index 0000000000..cd0f836585 --- /dev/null +++ b/apps/multiplatform/spec/impact.md @@ -0,0 +1,532 @@ +# SimpleX Chat Android & Desktop -- Impact Graph + +> Source file to product concept mapping. Use this to identify which product documents must be updated when a source file changes. +> +> Covers Kotlin Multiplatform (Compose) sources: commonMain, androidMain, desktopMain, and the Android and Desktop app modules. Also covers the shared Haskell core. + +--- + +## Product Concept Legend + +| ID | Concept | +|----|---------| +| PC1 | Chat List | +| PC2 | Direct Chat | +| PC3 | Group Chat | +| PC4 | Message Composition | +| PC5 | Message Reactions | +| PC6 | Message Editing | +| PC7 | Message Deletion | +| PC8 | Timed Messages | +| PC9 | Voice Messages | +| PC10 | File Transfer | +| PC11 | Link Previews | +| PC12 | Contact Connection | +| PC13 | Contact Verification | +| PC14 | Group Management | +| PC15 | Group Links | +| PC16 | Member Roles | +| PC17 | Audio/Video Calls | +| PC18 | Notifications | +| PC19 | User Profiles | +| PC20 | Incognito Mode | +| PC21 | Hidden Profiles | +| PC22 | Local Authentication | +| PC23 | Database Encryption | +| PC24 | Theme System | +| PC25 | Network Configuration | +| PC26 | Device Migration | +| PC27 | Remote Desktop | +| PC28 | Chat Tags | +| PC29 | User Address | +| PC30 | Member Support Chat | + +--- + +## 1. Common Sources (commonMain) + +Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` + +### 1.1 Core Model & Platform + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `App.kt` | PC1 through PC30 | High | Root composable — navigation scaffold for all features | +| `AppLock.kt` | PC22 | Medium | App lock state and authorization lifecycle | +| `model/ChatModel.kt` | PC1 through PC30 | High | Central state object — every feature reads or writes here | +| `model/SimpleXAPI.kt` | PC1 through PC30 | High | FFI bridge to Haskell core — all commands and responses | +| `model/CryptoFile.kt` | PC10, PC23 | Medium | Encrypted file read/write helpers | +| `platform/Core.kt` | PC1 through PC30 | High | Native FFI declarations (`chatMigrateInit`, `chatSendCmd`, etc.) — all API traffic | +| `platform/AppCommon.kt` | PC1 through PC30 | Medium | Shared app initialization logic | +| `platform/Files.kt` | PC10, PC23, PC26 | Medium | File path resolution, temp dirs, encryption utilities | +| `platform/NtfManager.kt` | PC18 | High | Notification manager expect declarations | +| `platform/Notifications.kt` | PC18 | Medium | Notification channel and permission abstractions | +| `platform/SimplexService.kt` | PC18 | Medium | Background service expect declarations | +| `platform/RecAndPlay.kt` | PC9 | Medium | Audio recording and playback abstractions | +| `platform/VideoPlayer.kt` | PC10, PC17 | Low | Video playback abstractions | +| `platform/Cryptor.kt` | PC23 | Medium | Keystore encryption expect declarations | +| `platform/Share.kt` | PC10, PC12 | Low | Share sheet abstractions | +| `platform/Images.kt` | PC10, PC19 | Low | Image processing utilities | +| `platform/Platform.kt` | PC1 through PC30 | Low | Platform detection and capability flags | +| `platform/PlatformTextField.kt` | PC4 | Low | Native text input expect declarations | +| `platform/Back.kt` | PC1 | Low | Back navigation handling | +| `platform/UI.kt` | PC24 | Low | UI density and locale helpers | +| `platform/ScrollableColumn.kt` | PC1 | Low | Scrollable list abstractions | +| `platform/Log.kt` | — | Low | Logging utility — no direct product impact | +| `platform/Modifier.kt` | PC24 | Low | Compose modifier extensions | +| `platform/Resources.kt` | PC24 | Low | Resource loading helpers | + +### 1.2 Theme + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `ui/theme/ThemeManager.kt` | PC24 | Medium | Theme resolution engine — all color and wallpaper logic | +| `ui/theme/Theme.kt` | PC24 | Medium | Theme composables and `SimpleXTheme` | +| `ui/theme/Color.kt` | PC24 | Low | Color palette definitions | +| `ui/theme/Shape.kt` | PC24 | Low | Shape token definitions | +| `ui/theme/Type.kt` | PC24 | Low | Typography definitions | + +### 1.3 Views — Chat List + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/chatlist/ChatListView.kt` | PC1, PC28 | High | Main screen — chat list rendering and search | +| `views/chatlist/ChatListNavLinkView.kt` | PC1, PC2, PC3 | Medium | Navigation from chat list item to chat | +| `views/chatlist/ChatPreviewView.kt` | PC1, PC2, PC3, PC11 | Medium | Chat row preview rendering | +| `views/chatlist/TagListView.kt` | PC28 | Medium | Chat tag filter UI | +| `views/chatlist/UserPicker.kt` | PC19, PC21 | Medium | Multi-profile switcher overlay | +| `views/chatlist/ShareListView.kt` | PC10 | Low | Share target list | +| `views/chatlist/ShareListNavLinkView.kt` | PC10 | Low | Share target navigation | +| `views/chatlist/ChatHelpView.kt` | PC1 | Low | Empty-state help content | +| `views/chatlist/ContactRequestView.kt` | PC12 | Medium | Incoming contact request row | +| `views/chatlist/ContactConnectionView.kt` | PC12 | Low | Pending connection row | +| `views/chatlist/ServersSummaryView.kt` | PC25 | Low | Server status summary | + +### 1.4 Views — Chat & Messaging + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/chat/ChatView.kt` | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11 | High | Core conversation UI — most messaging features | +| `views/chat/ComposeView.kt` | PC4, PC6, PC9, PC10, PC11 | High | Message composition — send path for all messages | +| `views/chat/SendMsgView.kt` | PC4, PC9 | Medium | Send button and voice record toggle | +| `views/chat/ComposeVoiceView.kt` | PC9 | Medium | Voice message recording UI | +| `views/chat/ComposeFileView.kt` | PC10 | Low | File attachment preview in compose area | +| `views/chat/ComposeImageView.kt` | PC10 | Low | Image attachment preview in compose area | +| `views/chat/ContextItemView.kt` | PC6 | Low | Reply/edit quote preview | +| `views/chat/SelectableChatItemToolbars.kt` | PC7, PC10 | Medium | Multi-select toolbar (delete, forward) | +| `views/chat/ChatInfoView.kt` | PC2, PC13, PC20 | Medium | Contact details and verification | +| `views/chat/ContactPreferences.kt` | PC2, PC8 | Medium | Per-contact feature preferences | +| `views/chat/ChatItemInfoView.kt` | PC2, PC3 | Low | Message delivery detail | +| `views/chat/ChatItemsLoader.kt` | PC2, PC3 | Medium | Pagination and message loading logic | +| `views/chat/ChatItemsMerger.kt` | PC2, PC3 | Medium | Merges incremental message updates | +| `views/chat/VerifyCodeView.kt` | PC13 | Medium | Contact security code verification | +| `views/chat/ScanCodeView.kt` | PC13 | Low | QR code scanning for verification | +| `views/chat/CommandsMenuView.kt` | PC4 | Low | Slash-command menu | +| `views/chat/ComposeContextProfilePickerView.kt` | PC20 | Low | Incognito profile picker in compose | +| `views/chat/ComposeContextPendingMemberActionsView.kt` | PC14, PC30 | Low | Pending member action buttons in compose | +| `views/chat/ComposeContextGroupDirectInvitationActionsView.kt` | PC14 | Low | Direct invitation action buttons in compose | +| `views/chat/ComposeContextContactRequestActionsView.kt` | PC12 | Low | Contact request action buttons in compose | + +### 1.5 Views — Chat Items + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/chat/item/ChatItemView.kt` | PC2, PC3, PC5, PC6, PC7, PC8 | High | Root chat item renderer with context menus | +| `views/chat/item/TextItemView.kt` | PC2, PC3, PC4 | Medium | Text message bubble rendering | +| `views/chat/item/FramedItemView.kt` | PC4, PC6, PC10, PC11 | Medium | Framed (quoted/forwarded) message container | +| `views/chat/item/CIImageView.kt` | PC10 | Medium | Image message rendering | +| `views/chat/item/CIVideoView.kt` | PC10 | Medium | Video message rendering | +| `views/chat/item/CIFileView.kt` | PC10 | Medium | File message rendering | +| `views/chat/item/CIVoiceView.kt` | PC9 | Medium | Voice message rendering and playback | +| `views/chat/item/EmojiItemView.kt` | PC5 | Low | Emoji reaction display | +| `views/chat/item/CIMetaView.kt` | PC2, PC3, PC8 | Low | Timestamp, delivery status, timed message indicator | +| `views/chat/item/CICallItemView.kt` | PC17 | Low | Call event item rendering | +| `views/chat/item/CIEventView.kt` | PC3, PC14, PC16 | Low | Group event item rendering | +| `views/chat/item/CIGroupInvitationView.kt` | PC3, PC14 | Low | Group invitation item rendering | +| `views/chat/item/CIMemberCreatedContactView.kt` | PC3, PC12 | Low | Member-created contact event | +| `views/chat/item/CIChatFeatureView.kt` | PC8 | Low | Feature change event rendering | +| `views/chat/item/CIFeaturePreferenceView.kt` | PC8 | Low | Feature preference change rendering | +| `views/chat/item/CIRcvDecryptionError.kt` | PC2, PC3 | Low | Decryption error display | +| `views/chat/item/DeletedItemView.kt` | PC7 | Low | Deleted message placeholder | +| `views/chat/item/MarkedDeletedItemView.kt` | PC7 | Low | Moderated/marked-deleted placeholder | +| `views/chat/item/ImageFullScreenView.kt` | PC10 | Low | Full-screen image viewer | +| `views/chat/item/CIBrokenComposableView.kt` | — | Low | Fallback for render failures | +| `views/chat/item/CIInvalidJSONView.kt` | — | Low | Fallback for malformed items | +| `views/chat/item/IntegrityErrorItemView.kt` | PC2, PC3 | Low | Message integrity error display | + +### 1.6 Views — Groups + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/chat/group/GroupChatInfoView.kt` | PC3, PC14, PC15, PC16, PC30 | High | Group management hub | +| `views/chat/group/AddGroupMembersView.kt` | PC14, PC16 | Medium | Member invitation flow | +| `views/chat/group/GroupMemberInfoView.kt` | PC3, PC14, PC16, PC30 | Medium | Member details and role management | +| `views/chat/group/GroupProfileView.kt` | PC3, PC14 | Medium | Group profile editing | +| `views/chat/group/GroupLinkView.kt` | PC15 | Low | Group link creation and sharing | +| `views/chat/group/GroupPreferences.kt` | PC3, PC8, PC14 | Medium | Group feature toggles | +| `views/chat/group/GroupMentions.kt` | PC3, PC4 | Medium | @mention resolution and display | +| `views/chat/group/GroupMembersToolbar.kt` | PC3, PC14 | Low | Member list toolbar | +| `views/chat/group/GroupReportsView.kt` | PC3, PC14 | Low | Group content reports | +| `views/chat/group/MemberAdmission.kt` | PC14, PC16 | Medium | Member admission settings | +| `views/chat/group/MemberSupportView.kt` | PC30 | Medium | Member support chat toggle | +| `views/chat/group/MemberSupportChatView.kt` | PC30 | Medium | Member support chat conversation | +| `views/chat/group/WelcomeMessageView.kt` | PC3, PC14 | Low | Group welcome message editor | + +### 1.7 Views — Calls + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/call/CallView.kt` | PC17 | High | Call UI and WebRTC composable | +| `views/call/CallManager.kt` | PC17 | High | Call lifecycle management | +| `views/call/WebRTC.kt` | PC17 | High | WebRTC types and signaling | +| `views/call/IncomingCallAlertView.kt` | PC17, PC18 | Medium | Incoming call overlay | + +### 1.8 Views — New Chat & Contacts + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/newchat/NewChatView.kt` | PC12, PC29 | High | New connection creation — onramp for all contacts | +| `views/newchat/NewChatSheet.kt` | PC12 | Medium | Bottom sheet with connection options | +| `views/newchat/ConnectPlan.kt` | PC12, PC15 | Medium | Link parsing and connection plan resolution | +| `views/newchat/AddGroupView.kt` | PC3, PC14 | Medium | New group creation flow | +| `views/newchat/ContactConnectionInfoView.kt` | PC12 | Low | Pending connection details | +| `views/newchat/AddContactLearnMore.kt` | PC12 | Low | Educational content | +| `views/newchat/QRCode.kt` | PC12 | Low | QR code display | +| `views/newchat/QRCodeScanner.kt` | PC12 | Low | QR code camera scanner | +| `views/contacts/ContactListNavView.kt` | PC1, PC12 | Medium | Contact list navigation | +| `views/contacts/ContactPreviewView.kt` | PC12 | Low | Contact row preview | + +### 1.9 Views — User Settings + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/usersettings/SettingsView.kt` | PC18, PC22, PC23, PC24, PC25, PC29 | Medium | Settings navigation hub | +| `views/usersettings/Appearance.kt` | PC24 | Low | Theme and appearance customization | +| `views/usersettings/PrivacySettings.kt` | PC20, PC22 | Medium | Privacy and lock settings | +| `views/usersettings/UserProfileView.kt` | PC19 | Medium | Profile display name and image editing | +| `views/usersettings/UserProfilesView.kt` | PC19, PC21 | Medium | Multi-profile management | +| `views/usersettings/HiddenProfileView.kt` | PC21 | Medium | Hidden profile access | +| `views/usersettings/IncognitoView.kt` | PC20 | Low | Incognito mode explanation | +| `views/usersettings/UserAddressView.kt` | PC29 | Medium | User SimpleX address management | +| `views/usersettings/UserAddressLearnMore.kt` | PC29 | Low | Address educational content | +| `views/usersettings/NotificationsSettingsView.kt` | PC18 | Medium | Notification mode configuration | +| `views/usersettings/CallSettings.kt` | PC17 | Low | Call-related settings | +| `views/usersettings/Preferences.kt` | PC2, PC3, PC8 | Medium | Chat feature preferences UI | +| `views/usersettings/SetDeliveryReceiptsView.kt` | PC2 | Low | Delivery receipts toggle | +| `views/usersettings/RTCServers.kt` | PC17, PC25 | Medium | WebRTC ICE server configuration | +| `views/usersettings/DeveloperView.kt` | — | Low | Developer/debug settings | +| `views/usersettings/HelpView.kt` | — | Low | Help and support links | +| `views/usersettings/MarkdownHelpView.kt` | PC4 | Low | Markdown formatting guide | +| `views/usersettings/VersionInfoView.kt` | — | Low | Version display | +| `views/usersettings/networkAndServers/NetworkAndServers.kt` | PC25 | High | Server and network configuration hub | +| `views/usersettings/networkAndServers/AdvancedNetworkSettings.kt` | PC25 | Medium | SOCKS proxy, timeouts, etc. | +| `views/usersettings/networkAndServers/OperatorView.kt` | PC25 | Medium | Server operator management | +| `views/usersettings/networkAndServers/ProtocolServersView.kt` | PC25 | Medium | SMP/XFTP server list | +| `views/usersettings/networkAndServers/ProtocolServerView.kt` | PC25 | Low | Individual server editing | +| `views/usersettings/networkAndServers/NewServerView.kt` | PC25 | Low | Add new server | +| `views/usersettings/networkAndServers/ScanProtocolServer.kt` | PC25 | Low | QR scan for server address | + +### 1.10 Views — Database & Migration + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/database/DatabaseView.kt` | PC23, PC26 | High | Database management — export, import, passphrase | +| `views/database/DatabaseEncryptionView.kt` | PC23 | High | Database encryption passphrase change | +| `views/database/DatabaseErrorView.kt` | PC23 | Medium | Database open error recovery | +| `views/migration/MigrateFromDevice.kt` | PC26 | High | Outbound device migration | +| `views/migration/MigrateToDevice.kt` | PC26 | High | Inbound device migration | + +### 1.11 Views — Local Auth & Onboarding + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/localauth/LocalAuthView.kt` | PC22 | Medium | App lock authentication flow | +| `views/localauth/SetAppPasscodeView.kt` | PC22 | Medium | Passcode creation and change | +| `views/localauth/PasscodeView.kt` | PC22 | Medium | Passcode entry UI | +| `views/localauth/PasswordEntry.kt` | PC22 | Low | Password input field | +| `views/onboarding/OnboardingView.kt` | PC1 | Medium | Onboarding flow navigation | +| `views/onboarding/SimpleXInfo.kt` | PC1 | Low | Welcome screen | +| `views/onboarding/SetNotificationsMode.kt` | PC18 | Medium | Notification permission and mode setup | +| `views/onboarding/SetupDatabasePassphrase.kt` | PC23 | Medium | Initial database passphrase setup | +| `views/onboarding/ChooseServerOperators.kt` | PC25 | Medium | Initial server operator selection | +| `views/onboarding/WhatsNewView.kt` | — | Low | Release notes display | +| `views/onboarding/HowItWorks.kt` | — | Low | Educational content | +| `views/onboarding/LinkAMobileView.kt` | PC27 | Low | Mobile linking onboarding | + +### 1.12 Views — Remote Desktop + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/remote/ConnectDesktopView.kt` | PC27 | Medium | Connect-to-desktop flow (from mobile) | +| `views/remote/ConnectMobileView.kt` | PC27 | Medium | Connect-to-mobile flow (from desktop) | + +### 1.13 Views — Helpers + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/helpers/AlertManager.kt` | PC1 through PC30 | Medium | Modal alert system used across all features | +| `views/helpers/ModalView.kt` | PC1 through PC30 | Medium | Modal navigation stack | +| `views/helpers/Utils.kt` | PC1 through PC30 | Low | Shared formatting, clipboard, and utility functions | +| `views/helpers/DatabaseUtils.kt` | PC23 | Medium | Keystore passphrase and database helpers | +| `views/helpers/LinkPreviews.kt` | PC11 | Medium | Link preview fetching and rendering | +| `views/helpers/LocalAuthentication.kt` | PC22 | Medium | Biometric/passcode authentication expect | +| `views/helpers/ChatWallpaper.kt` | PC24 | Low | Chat wallpaper rendering | +| `views/helpers/ChatInfoImage.kt` | PC19 | Low | Profile image composable | +| `views/helpers/ThemeModeEditor.kt` | PC24 | Low | Theme mode toggle | +| `views/helpers/ChooseAttachmentView.kt` | PC10 | Low | Attachment picker | +| `views/helpers/GetImageView.kt` | PC10, PC19 | Low | Image capture and crop | +| `views/helpers/TextEditor.kt` | PC4 | Low | Rich text editor helpers | +| `views/helpers/SearchTextField.kt` | PC1 | Low | Search bar composable | +| `views/helpers/CustomTimePicker.kt` | PC8 | Low | Time picker for timed messages | +| `views/helpers/DragAndDrop.kt` | PC10 | Low | Drag-and-drop file handling | +| `views/helpers/ProcessedErrors.kt` | — | Low | Error aggregation | +| `views/helpers/AnimationUtils.kt` | PC24 | Low | Animation helpers | +| `views/helpers/DefaultDialog.kt` | — | Low | Dialog composable primitives | +| `views/helpers/DefaultDropdownMenu.kt` | — | Low | Dropdown menu composable | +| `views/helpers/Section.kt` | — | Low | Settings section composable | +| `views/helpers/SimpleButton.kt` | — | Low | Button composable | +| `views/helpers/DefaultTopAppBar.kt` | — | Low | App bar composable | +| `views/helpers/DefaultBasicTextField.kt` | PC4 | Low | Text field composable | +| `views/helpers/AppBarTitle.kt` | — | Low | App bar title composable | +| `views/helpers/BlurModifier.kt` | PC22 | Low | Blur modifier for app lock | +| `views/helpers/CollapsingAppBar.kt` | — | Low | Collapsing toolbar composable | +| `views/helpers/CustomIcons.kt` | — | Low | Custom icon definitions | +| `views/helpers/DataClasses.kt` | — | Low | Shared data class utilities | +| `views/helpers/DefaultProgressBar.kt` | — | Low | Progress bar composable | +| `views/helpers/DefaultSwitch.kt` | — | Low | Switch composable | +| `views/helpers/Enums.kt` | — | Low | Enum utility extensions | +| `views/helpers/ExposedDropDownSettingRow.kt` | — | Low | Dropdown setting row composable | +| `views/helpers/GestureDetector.kt` | — | Low | Touch gesture utilities | +| `views/helpers/Modifiers.kt` | — | Low | Compose modifier extensions | +| `views/helpers/SubscriptionStatusIcon.kt` | PC25 | Low | Server connection status icon | + +### 1.14 Views — Other + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `views/TerminalView.kt` | — | Low | Developer chat console | +| `views/SplashView.kt` | — | Low | Splash screen | +| `views/WelcomeView.kt` | PC1 | Low | Empty-state welcome | +| `views/Preview.kt` | — | Low | Compose preview utilities | + +--- + +## 2. Android Sources + +### 2.1 Android App Module + +Path prefix: `android/src/main/java/chat/simplex/app/` + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `SimplexApp.kt` | PC1 through PC30 | High | Application class — initializes core, preferences, and notification channels | +| `MainActivity.kt` | PC1 through PC30 | High | Single-activity host — intent handling, lifecycle, deep links | +| `SimplexService.kt` | PC18 | High | Foreground service — keeps message receiver alive | +| `CallService.kt` | PC17 | Medium | Foreground service for active calls | +| `MessagesFetcherWorker.kt` | PC18 | Medium | WorkManager periodic message fetch | +| `model/NtfManager.android.kt` | PC18 | High | Android notification channels, display, and actions | +| `views/call/CallActivity.kt` | PC17 | Medium | Dedicated activity for full-screen call UI | +| `views/helpers/Util.kt` | — | Low | Android-specific utility extensions | + +### 2.2 Android Platform Implementations (androidMain) + +Path prefix: `common/src/androidMain/kotlin/chat/simplex/common/` + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `platform/AppCommon.android.kt` | PC1 through PC30 | Medium | Android app initialization actual declarations | +| `platform/SimplexService.android.kt` | PC18 | Medium | Android foreground service actual implementation | +| `platform/Files.android.kt` | PC10, PC23, PC26 | Medium | Android file paths and content-URI resolution | +| `platform/Cryptor.android.kt` | PC23 | Medium | Android Keystore encryption actual implementation | +| `platform/RecAndPlay.android.kt` | PC9 | Medium | Android MediaRecorder/MediaPlayer actual implementation | +| `platform/VideoPlayer.android.kt` | PC10 | Low | Android ExoPlayer actual implementation | +| `platform/Notifications.android.kt` | PC18 | Medium | Android notification channel creation | +| `platform/Images.android.kt` | PC10, PC19 | Low | Android bitmap processing | +| `platform/PlatformTextField.android.kt` | PC4 | Low | Android native text field actual implementation | +| `platform/Share.android.kt` | PC10 | Low | Android share intent actual implementation | +| `platform/Back.android.kt` | PC1 | Low | Android back press handler | +| `platform/UI.android.kt` | PC24 | Low | Android density and locale | +| `platform/ScrollableColumn.android.kt` | PC1 | Low | Android lazy list actual implementation | +| `platform/Log.android.kt` | — | Low | Android Log wrapper | +| `platform/Modifier.android.kt` | — | Low | Android modifier extensions | +| `platform/Resources.android.kt` | — | Low | Android resource loading | +| `helpers/NetworkObserver.kt` | PC25 | Medium | Android ConnectivityManager observer | +| `helpers/Permissions.kt` | PC9, PC10, PC17, PC18 | Medium | Android runtime permission requests | +| `helpers/SoundPlayer.kt` | PC17, PC18 | Low | Android sound playback for calls and notifications | +| `helpers/Extensions.kt` | — | Low | Kotlin extension utilities | +| `helpers/Locale.kt` | — | Low | Locale helpers | +| `views/call/CallView.android.kt` | PC17 | Medium | Android WebView-based WebRTC call | +| `views/call/CallAudioDeviceManager.kt` | PC17 | Medium | Android audio routing (speaker, earpiece, bluetooth) | +| `views/chat/ComposeView.android.kt` | PC4, PC10 | Low | Android compose view extensions | +| `views/chat/SendMsgView.android.kt` | PC4 | Low | Android send button extensions | +| `views/chat/item/ChatItemView.android.kt` | PC2, PC3 | Low | Android chat item extensions | +| `views/chat/item/CIImageView.android.kt` | PC10 | Low | Android image rendering extensions | +| `views/chat/item/CIVideoView.android.kt` | PC10 | Low | Android video rendering extensions | +| `views/chat/item/CIFileView.android.kt` | PC10 | Low | Android file view extensions | +| `views/chat/item/EmojiItemView.android.kt` | PC5 | Low | Android emoji rendering extensions | +| `views/chat/item/ImageFullScreenView.android.kt` | PC10 | Low | Android full-screen image viewer | +| `views/chatlist/ChatListView.android.kt` | PC1 | Low | Android chat list extensions | +| `views/chatlist/ChatListNavLinkView.android.kt` | PC1 | Low | Android chat list navigation extensions | +| `views/chatlist/TagListView.android.kt` | PC28 | Low | Android tag list extensions | +| `views/chatlist/UserPicker.android.kt` | PC19 | Low | Android profile picker extensions | +| `views/database/DatabaseView.android.kt` | PC23, PC26 | Low | Android database view extensions | +| `views/database/DatabaseEncryptionView.android.kt` | PC23 | Low | Android encryption view extensions | +| `views/helpers/LocalAuthentication.android.kt` | PC22 | Medium | Android BiometricPrompt actual implementation | +| `views/helpers/ChooseAttachmentView.android.kt` | PC10 | Low | Android file/camera chooser | +| `views/helpers/GetImageView.android.kt` | PC10, PC19 | Low | Android image capture | +| `views/helpers/CustomTimePicker.android.kt` | PC8 | Low | Android time picker | +| `views/helpers/Utils.android.kt` | — | Low | Android utility extensions | +| `views/helpers/DefaultDialog.android.kt` | — | Low | Android dialog extensions | +| `views/helpers/WorkaroundFocusSearchLayout.kt` | — | Low | Android focus workaround | +| `views/newchat/QRCode.android.kt` | PC12 | Low | Android QR code rendering | +| `views/newchat/QRCodeScanner.android.kt` | PC12 | Low | Android camera QR scanner | +| `views/onboarding/SimpleXInfo.android.kt` | PC1 | Low | Android onboarding extensions | +| `views/onboarding/SetNotificationsMode.android.kt` | PC18 | Low | Android notification mode extensions | +| `views/usersettings/Appearance.android.kt` | PC24 | Low | Android appearance extensions | +| `views/usersettings/PrivacySettings.android.kt` | PC20, PC22 | Low | Android privacy settings extensions | +| `views/usersettings/SettingsView.android.kt` | — | Low | Android settings extensions | +| `views/usersettings/networkAndServers/OperatorView.android.kt` | PC25 | Low | Android operator view extensions | +| `views/usersettings/networkAndServers/ScanProtocolServer.android.kt` | PC25 | Low | Android server QR scan | +| `ui/theme/Theme.android.kt` | PC24 | Low | Android dynamic color / system theme | +| `ui/theme/Type.android.kt` | PC24 | Low | Android typography | + +--- + +## 3. Desktop Sources + +### 3.1 Desktop App Module + +Path prefix: `desktop/src/jvmMain/kotlin/chat/simplex/desktop/` + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `Main.kt` | PC1 through PC30 | High | JVM entry point — Haskell init, migrations, app launch | + +### 3.2 Desktop Platform Implementations (desktopMain) + +Path prefix: `common/src/desktopMain/kotlin/chat/simplex/common/` + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `DesktopApp.kt` | PC1, PC2, PC3 | High | Desktop Compose window — window lifecycle, crash recovery | +| `StoreWindowState.kt` | — | Low | Window position/size persistence | +| `model/NtfManager.desktop.kt` | PC18 | Medium | Desktop system tray notification display | +| `platform/AppCommon.desktop.kt` | PC1 through PC30 | Medium | Desktop app initialization actual declarations | +| `platform/SimplexService.desktop.kt` | PC18 | Low | Desktop background receiver (no foreground service) | +| `platform/Files.desktop.kt` | PC10, PC23, PC26 | Medium | Desktop file path resolution | +| `platform/Cryptor.desktop.kt` | PC23 | Medium | Desktop keystore encryption actual implementation | +| `platform/RecAndPlay.desktop.kt` | PC9 | Medium | Desktop audio recording/playback actual implementation | +| `platform/VideoPlayer.desktop.kt` | PC10 | Low | Desktop VLC-based video player | +| `platform/Videos.desktop.kt` | PC10 | Low | Desktop video utilities | +| `platform/Notifications.desktop.kt` | PC18 | Low | Desktop notification setup | +| `platform/Images.desktop.kt` | PC10 | Low | Desktop image processing | +| `platform/PlatformTextField.desktop.kt` | PC4 | Low | Desktop text field actual implementation | +| `platform/Share.desktop.kt` | PC10 | Low | Desktop clipboard/share | +| `platform/Back.desktop.kt` | PC1 | Low | Desktop back navigation | +| `platform/UI.desktop.kt` | PC24 | Low | Desktop density and locale | +| `platform/ScrollableColumn.desktop.kt` | PC1 | Low | Desktop lazy list | +| `platform/Platform.desktop.kt` | — | Low | Platform detection | +| `platform/Log.desktop.kt` | — | Low | Desktop log output | +| `platform/Modifier.desktop.kt` | — | Low | Desktop modifier extensions | +| `platform/Resources.desktop.kt` | — | Low | Desktop resource loading | +| `views/call/CallView.desktop.kt` | PC17 | Medium | Desktop WebView-based WebRTC call | +| `views/chat/ComposeView.desktop.kt` | PC4, PC10 | Low | Desktop compose view (drag-and-drop, paste) | +| `views/chat/SendMsgView.desktop.kt` | PC4 | Low | Desktop send shortcut (Enter key handling) | +| `views/chat/item/ChatItemView.desktop.kt` | PC2, PC3 | Low | Desktop chat item extensions | +| `views/chat/item/CIImageView.desktop.kt` | PC10 | Low | Desktop image rendering | +| `views/chat/item/CIVideoView.desktop.kt` | PC10 | Low | Desktop video rendering | +| `views/chat/item/CIFileView.desktop.kt` | PC10 | Low | Desktop file open/save | +| `views/chat/item/EmojiItemView.desktop.kt` | PC5 | Low | Desktop emoji rendering | +| `views/chat/item/ImageFullScreenView.desktop.kt` | PC10 | Low | Desktop full-screen image | +| `views/chatlist/ChatListView.desktop.kt` | PC1 | Low | Desktop chat list extensions | +| `views/chatlist/ChatListNavLinkView.desktop.kt` | PC1 | Low | Desktop chat list navigation | +| `views/chatlist/TagListView.desktop.kt` | PC28 | Low | Desktop tag list extensions | +| `views/chatlist/UserPicker.desktop.kt` | PC19 | Low | Desktop profile picker | +| `views/database/DatabaseView.desktop.kt` | PC23, PC26 | Low | Desktop database view extensions | +| `views/database/DatabaseEncryptionView.desktop.kt` | PC23 | Low | Desktop encryption view extensions | +| `views/helpers/AppUpdater.kt` | — | Low | Desktop auto-update checker and installer | +| `views/helpers/OkHttpProgressListener.kt` | — | Low | Download progress tracking for updates | +| `views/helpers/LocalAuthentication.desktop.kt` | PC22 | Low | Desktop passcode-only auth (no biometrics) | +| `views/helpers/ChooseAttachmentView.desktop.kt` | PC10 | Low | Desktop file chooser dialog | +| `views/helpers/GetImageView.desktop.kt` | PC10, PC19 | Low | Desktop image file picker | +| `views/helpers/CustomTimePicker.desktop.kt` | PC8 | Low | Desktop time picker | +| `views/helpers/Utils.desktop.kt` | — | Low | Desktop utility extensions | +| `views/helpers/DefaultDialog.desktop.kt` | — | Low | Desktop dialog extensions | +| `views/newchat/QRCode.desktop.kt` | PC12 | Low | Desktop QR code rendering | +| `views/newchat/QRCodeScanner.desktop.kt` | PC12 | Low | Desktop QR code scanner (screen/clipboard) | +| `views/onboarding/SimpleXInfo.desktop.kt` | PC1 | Low | Desktop onboarding extensions | +| `views/onboarding/SetNotificationsMode.desktop.kt` | PC18 | Low | Desktop notification mode extensions | +| `views/usersettings/Appearance.desktop.kt` | PC24 | Low | Desktop appearance extensions | +| `views/usersettings/PrivacySettings.desktop.kt` | PC20, PC22 | Low | Desktop privacy settings extensions | +| `views/usersettings/SettingsView.desktop.kt` | — | Low | Desktop settings extensions | +| `views/usersettings/networkAndServers/OperatorView.desktop.kt` | PC25 | Low | Desktop operator view extensions | +| `views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt` | PC25 | Low | Desktop server address scan | +| `ui/theme/Theme.desktop.kt` | PC24 | Low | Desktop system theme detection | +| `ui/theme/Type.desktop.kt` | PC24 | Low | Desktop typography | +| `other/videoplayer/SkiaBitmapVideoSurface.kt` | PC10 | Low | Desktop Skia video surface for VLC | + +--- + +## 4. Haskell Core Impact + +The Haskell core is compiled as a shared native library (`libsimplex.so` / `libsimplex.dylib`) and linked via JNI through `Core.kt`. Changes here affect both Android and Desktop identically. + +| Source File | Product Concepts Affected | Risk Level | Notes | +|-------------|--------------------------|------------|-------| +| `src/Simplex/Chat.hs` | PC1 through PC30 | High | Main chat module — top-level orchestration | +| `src/Simplex/Chat/Controller.hs` | PC1 through PC30 | High | Command processor — all API commands dispatched here | +| `src/Simplex/Chat/Types.hs` | PC1 through PC30 | High | Core data types shared across all features | +| `src/Simplex/Chat/Core.hs` | PC1 through PC30 | High | Chat engine lifecycle (start, stop, subscribe) | +| `src/Simplex/Chat/Library/Commands.hs` | PC1 through PC30 | High | API command handler implementations | +| `src/Simplex/Chat/Library/Internal.hs` | PC1 through PC30 | High | Internal helpers for command processing | +| `src/Simplex/Chat/Library/Subscriber.hs` | PC1 through PC30 | High | Event subscriber — incoming message routing | +| `src/Simplex/Chat/Protocol.hs` | PC2, PC3, PC4, PC5, PC6, PC7 | High | Chat-level message protocol (x-events) | +| `src/Simplex/Chat/Messages.hs` | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9 | High | Message types and content | +| `src/Simplex/Chat/Messages/CIContent.hs` | PC4, PC5, PC6, PC7, PC8, PC9, PC11 | Medium | Chat item content variants | +| `src/Simplex/Chat/Messages/CIContent/Events.hs` | PC3, PC14, PC16 | Medium | Group event content types | +| `src/Simplex/Chat/Messages/Batch.hs` | PC2, PC3, PC4 | Medium | Message batching for efficient delivery | +| `src/Simplex/Chat/Call.hs` | PC17 | Medium | Call signaling types | +| `src/Simplex/Chat/Files.hs` | PC10 | Medium | File transfer orchestration | +| `src/Simplex/Chat/Delivery.hs` | PC2, PC3 | Medium | Message delivery engine | +| `src/Simplex/Chat/Markdown.hs` | PC4 | Low | Markdown parsing for message formatting | +| `src/Simplex/Chat/Store.hs` | PC1 through PC30 | High | Database store interface | +| `src/Simplex/Chat/Store/Shared.hs` | PC1 through PC30 | Medium | Shared store utilities | +| `src/Simplex/Chat/Store/Messages.hs` | PC4, PC5, PC6, PC7, PC8 | High | Message persistence | +| `src/Simplex/Chat/Store/Groups.hs` | PC3, PC14, PC15, PC16, PC30 | High | Group persistence | +| `src/Simplex/Chat/Store/Direct.hs` | PC2, PC12, PC13 | High | Contact persistence | +| `src/Simplex/Chat/Store/Files.hs` | PC10 | Medium | File transfer persistence | +| `src/Simplex/Chat/Store/Profiles.hs` | PC19, PC21 | Medium | User profile persistence | +| `src/Simplex/Chat/Store/Connections.hs` | PC2, PC12 | High | Connection persistence and entity resolution | +| `src/Simplex/Chat/Store/ContactRequest.hs` | PC12 | Medium | Contact request persistence | +| `src/Simplex/Chat/Store/NoteFolders.hs` | PC1 | Low | Note folder (self-chat) persistence | +| `src/Simplex/Chat/Store/Delivery.hs` | PC2, PC3 | Medium | Delivery task persistence | +| `src/Simplex/Chat/Store/AppSettings.hs` | PC25 | Low | App settings persistence | +| `src/Simplex/Chat/Store/Remote.hs` | PC27 | Low | Remote desktop session persistence | +| `src/Simplex/Chat/Archive.hs` | PC26 | Medium | Database export/import for migration | +| `src/Simplex/Chat/Options.hs` | PC23, PC25 | Low | Startup options (DB path, key, etc.) | +| `src/Simplex/Chat/Remote.hs` | PC27 | Medium | Remote desktop protocol handler | +| `src/Simplex/Chat/Remote/Types.hs` | PC27 | Low | Remote desktop data types | +| `src/Simplex/Chat/Remote/Protocol.hs` | PC27 | Medium | Remote desktop wire protocol | +| `src/Simplex/Chat/Remote/Transport.hs` | PC27 | Low | Remote desktop transport layer | +| `src/Simplex/Chat/Remote/RevHTTP.hs` | PC27 | Low | Reverse HTTP for remote desktop | +| `src/Simplex/Chat/Remote/AppVersion.hs` | PC27 | Low | Remote version negotiation | +| `src/Simplex/Chat/ProfileGenerator.hs` | PC20 | Low | Random profile generation for incognito | +| `src/Simplex/Chat/Types/UITheme.hs` | PC24 | Low | Theme data types for UI customization | +| `src/Simplex/Chat/Types/Preferences.hs` | PC2, PC3, PC8 | Medium | Chat feature preferences (timed messages, etc.) | +| `src/Simplex/Chat/Types/Shared.hs` | PC3, PC16 | Medium | Shared types including GroupMemberRole | +| `src/Simplex/Chat/Types/MemberRelations.hs` | PC3, PC16, PC30 | Medium | Member relationship state machine | +| `src/Simplex/Chat/Operators.hs` | PC25 | Medium | Server operator management | +| `src/Simplex/Chat/Operators/Presets.hs` | PC25 | Low | Preset server operators | +| `src/Simplex/Chat/Operators/Conditions.hs` | PC25 | Low | Operator usage conditions | +| `src/Simplex/Chat/AppSettings.hs` | PC25 | Low | App settings sync types | +| `src/Simplex/Chat/Mobile.hs` | PC1 through PC30 | High | C FFI exports — JNI bridge target | +| `src/Simplex/Chat/Mobile/File.hs` | PC10 | Medium | Mobile file read/write FFI | +| `src/Simplex/Chat/Mobile/Shared.hs` | PC1 through PC30 | Medium | Shared FFI helpers | +| `src/Simplex/Chat/Mobile/WebRTC.hs` | PC17 | Low | WebRTC FFI helpers | +| `src/Simplex/Chat/View.hs` | PC1 through PC30 | Low | Terminal view rendering (not used by mobile/desktop UI) | +| `src/Simplex/Chat/Stats.hs` | PC25 | Low | Server statistics tracking | +| `src/Simplex/Chat/Util.hs` | — | Low | General Haskell utilities | +| `src/Simplex/Chat/Styled.hs` | — | Low | Terminal styled text (not used by mobile/desktop UI) | +| `src/Simplex/Chat/Help.hs` | — | Low | Terminal help text | +| `src/Simplex/Chat/Bot.hs` | — | Low | Chat bot framework | +| `src/Simplex/Chat/Bot/KnownContacts.hs` | — | Low | Bot known contacts | diff --git a/apps/multiplatform/spec/services/calls.md b/apps/multiplatform/spec/services/calls.md new file mode 100644 index 0000000000..a8d056ebea --- /dev/null +++ b/apps/multiplatform/spec/services/calls.md @@ -0,0 +1,175 @@ +# WebRTC Calling Service + +## Table of Contents + +1. [Overview](#1-overview) +2. [Call State Machine](#2-call-state-machine) +3. [Android Implementation](#3-android-implementation) +4. [Desktop Implementation](#4-desktop-implementation) +5. [Common Call API](#5-common-call-api) +6. [IncomingCallAlertView](#6-incomingcallalertview) +7. [Source Files](#7-source-files) + +## Executive Summary + +WebRTC calling in SimpleX Chat operates over SMP (SimpleX Messaging Protocol) for signaling, with platform-specific WebRTC media implementations. Android uses a WebView-based approach with a dedicated `CallActivity` and foreground `CallService`, while Desktop opens the system browser and communicates via a NanoWSD WebSocket server on localhost. Both platforms share a common `CallManager` for call lifecycle and a `CallState` enum for state tracking. Call commands and responses are serialized as JSON and exchanged between the native layer and the WebRTC JavaScript layer. + +--- + +## 1. Overview + +Call signaling uses the same SMP protocol on all platforms -- call invitations, offers, answers, ICE candidates, and status updates flow through the chat backend via API commands. The WebRTC media plane, however, is implemented differently per platform: + +- **Android**: WebView loads `call.html` from bundled assets; a `@JavascriptInterface` bridge (`WebRTCInterface`) forwards JSON messages between Kotlin and JavaScript. +- **Desktop**: The system browser opens `http://localhost:50395/simplex/call/`; a NanoWSD HTTP+WebSocket server serves `call.html` from classpath resources and relays JSON commands/responses over WebSocket. + +Both platforms share the [`CallManager`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt) class (119 lines), which orchestrates incoming call acceptance, call ending, and notification management. + +--- + + + +## 2. Call State Machine + +Defined in [`WebRTC.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt#L50): + +``` +enum class CallState { + WaitCapabilities, // Call initiated, waiting for local WebRTC capabilities + InvitationSent, // Invitation sent to peer via SMP + InvitationAccepted, // Peer's invitation accepted locally + OfferSent, // SDP offer sent to peer + OfferReceived, // SDP offer received from peer + AnswerReceived, // SDP answer received from peer + Negotiated, // ICE negotiation in progress + Connected, // Media flowing + Ended; // Call terminated +} +``` + +**Outgoing call flow**: `WaitCapabilities` -> `InvitationSent` -> `OfferSent` -> `AnswerReceived` -> `Negotiated` -> `Connected` -> `Ended` + +**Incoming call flow**: `InvitationAccepted` -> `OfferReceived` -> `Negotiated` -> `Connected` -> `Ended` + +State transitions are driven by `WCallResponse` messages from the WebRTC layer. Each transition typically triggers a corresponding API command (e.g., `apiSendCallInvitation`, `apiSendCallOffer`). + +--- + + + +## 3. Android Implementation + +### 3.1 CallActivity.kt (464 lines) + +[`CallActivity.kt`](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt) + +A dedicated `ComponentActivity` that hosts the call UI. Key responsibilities: + +- **Intent handling** ([line 64](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L64)): On `AcceptCallAction` intent, looks up the matching `RcvCallInvitation` and calls `callManager.acceptIncomingCall()`. +- **Lock screen support** ([line 160](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L160)): `unlockForIncomingCall()` uses `setShowWhenLocked(true)` / `setTurnScreenOn(true)` on API 27+, falls back to window flags on older versions. `lockAfterIncomingCall()` reverses these settings. +- **Picture-in-Picture** ([line 99](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L99)): `setPipParams()` configures PiP aspect ratio and source rect hint. On Android 12+ (`Build.VERSION_CODES.S`), auto-enter PiP is enabled for video calls. `onPictureInPictureModeChanged` toggles `activeCallViewIsCollapsed` and sends a `WCallCommand.Layout` command. +- **Permission checks** ([line 122](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L122)): Checks `RECORD_AUDIO` and conditionally `CAMERA` permissions. +- **Service binding** ([line 181](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L181)): Binds to `CallService` as a workaround for Android 12 background activity launch restrictions. +- **CallActivityView composable** ([line 208](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L208)): Renders `ActiveCallView()` when permissions are granted and a call is active. Shows `CallPermissionsView` when permissions are needed. Shows `IncomingCallLockScreenAlert` for incoming calls on the lock screen. + +### 3.2 CallService.kt (207 lines) + +[`CallService.kt`](../../android/src/main/java/chat/simplex/app/CallService.kt) + +An Android foreground `Service` that keeps the call alive when the app is backgrounded: + +- **Foreground notification** ([line 131](../../android/src/main/java/chat/simplex/app/CallService.kt#L131)): Shows contact name (respecting `NotificationPreviewMode`), call type (audio/video), a chronometer when connected, and an "End call" action button. +- **WakeLock** ([line 66](../../android/src/main/java/chat/simplex/app/CallService.kt#L66)): Acquires `PARTIAL_WAKE_LOCK` to prevent CPU sleep during calls. +- **Notification channel** ([line 121](../../android/src/main/java/chat/simplex/app/CallService.kt#L121)): Creates `CALL_NOTIFICATION_CHANNEL_ID` with `IMPORTANCE_DEFAULT`. +- **Foreground service type** ([line 100](../../android/src/main/java/chat/simplex/app/CallService.kt#L100)): Uses `MEDIA_PLAYBACK | MICROPHONE` (+ `CAMERA` for video) on API 30+, `REMOTE_MESSAGING` on API 34+ when no active call. +- **Binder** ([line 158](../../android/src/main/java/chat/simplex/app/CallService.kt#L158)): `CallServiceBinder` allows `CallActivity` to call `updateNotification()` when call state changes. +- **CallActionReceiver** ([line 170](../../android/src/main/java/chat/simplex/app/CallService.kt#L170)): `BroadcastReceiver` that handles the `EndCallAction` from the notification. + +### 3.3 CallView.android.kt (891 lines) + +[`CallView.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt) + +The `actual` platform implementation of `ActiveCallView()` and supporting composables: + +- **ActiveCallState** ([line 74](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L74)): Manages proximity lock (screen-off wake lock), `CallAudioDeviceManager` for audio routing (earpiece/speaker/bluetooth), `CallSoundsPlayer` for ringtones and vibration. Implements `Closeable` to clean up resources on call end. +- **ActiveCallView** ([line 114](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L114)): Renders `WebRTCView` plus `ActiveCallOverlay`. Handles `WCallResponse` messages and dispatches corresponding API calls. Manages volume control stream (`STREAM_VOICE_CALL`), screen keep-on, and call command lifecycle. +- **WebRTCView** ([line 691](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L691)): Creates/reuses a static `WebView` via `AndroidView`. Configures `WebViewAssetLoader` for local asset loading. Sets up `WebRTCInterface` JavaScript bridge. Loads `file:android_asset/www/android/call.html`. Processes `WCallCommand` queue by evaluating `processCommand()` JavaScript. +- **ActiveCallOverlayLayout** ([line 329](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L329)): Full overlay with mic toggle, speaker/device selector, end call, video toggle, and camera flip buttons. Adapts layout for video vs audio calls. +- **CallPermissionsView** ([line 569](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L569)): Handles runtime permission requests for microphone and camera with a fallback to settings if the system dialog is not shown. + +### 3.4 ActiveCallState + +[`ActiveCallState`](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt#L74) (line 74 of `CallView.android.kt`): + +| Component | Purpose | +|---|---| +| `proximityLock` | `PROXIMITY_SCREEN_OFF_WAKE_LOCK` -- turns screen off when phone is held to ear | +| `callAudioDeviceManager` | Manages audio routing between earpiece, speaker, Bluetooth, wired headset | +| `CallSoundsPlayer` | Plays connecting/ringing sounds and vibration patterns | +| `wasConnected` | Tracks if call ever connected (for end-of-call vibration) | +| `close()` | Stops sounds, vibrates on disconnect, releases proximity lock, clears audio manager overrides | + +--- + +## 4. Desktop Implementation + +### 4.1 CallView.desktop.kt (263 lines) + +[`CallView.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt) + +Desktop calls run WebRTC in the system browser, not an embedded WebView: + +- **NanoWSD server** ([line 209](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L209)): `startServer()` creates a `NanoWSD` instance bound to `localhost:50395`. The server serves `call.html` from JAR resources at `/assets/www/desktop/call.html` for the path `/simplex/call/`. All other paths serve resources from `/assets/www/`. +- **WebSocket communication** ([line 238](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L238)): `MyWebSocket` handles WebSocket frames from the browser. `onMessage` deserializes JSON into `WVAPIMessage` and forwards to the response handler. `onClose` triggers `WCallResponse.End`. +- **WebRTCController** ([line 153](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L153)): Opens `http://localhost:50395/simplex/call/` via `LocalUriHandler`. Processes `WCallCommand` queue by sending JSON over WebSocket to all active connections. On dispose, sends `WCallCommand.End` and stops the server. +- **SendStateUpdates** ([line 137](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L137)): Sends `WCallCommand.Description` with call state and encryption info text to the browser for display. +- **ActiveCallView** ([line 28](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L28)): Handles `WCallResponse` messages identically to Android (same state machine), plus a `WCallCommand.Permission` message on `Capabilities` error for browser permission denial guidance. + +--- + +## 5. Common Call API + +Defined in [`SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt): + +| Function | Line | Description | +|---|---|---| +| `apiGetCallInvitations` | [L1842](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1842) | Retrieve pending call invitations from the backend | +| `apiSendCallInvitation` | [L1849](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1849) | Send call invitation to a contact with `CallType` | +| `apiRejectCall` | [L1854](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1854) | Reject an incoming call | +| `apiSendCallOffer` | [L1859](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1859) | Send SDP offer with ICE candidates and capabilities | +| `apiSendCallAnswer` | [L1866](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1866) | Send SDP answer with ICE candidates | +| `apiSendCallExtraInfo` | [L1872](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1872) | Send additional ICE candidates discovered after initial exchange | +| `apiEndCall` | [L1878](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1878) | Terminate a call | +| `apiCallStatus` | [L1883](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1883) | Report WebRTC connection status to the backend | + +All functions send commands via `sendCmd()` to the chat core and return `Boolean` success status (except `apiGetCallInvitations` which returns `List`). + +--- + + + +## 6. IncomingCallAlertView + +[`IncomingCallAlertView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt) (128 lines) + +An in-app notification banner shown when a call invitation arrives while the app is in the foreground: + +- **IncomingCallAlertView** ([line 27](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt#L27)): Starts `SoundPlayer` for the ringtone (suppressed if already in a call view). Shows `IncomingCallAlertLayout`. +- **IncomingCallAlertLayout** ([line 49](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt#L49)): Colored banner with `ProfilePreview` of the caller, call type icon (audio/video), and three action buttons: Reject (red), Ignore (primary), Accept (green). +- **IncomingCallInfo** ([line 74](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt#L74)): Shows the user profile image (for multi-user), call media type icon, and call type text (encrypted/unencrypted audio/video). + +--- + +## 7. Source Files + +| File | Path | Lines | Description | +|---|---|---|---| +| `CallView.kt` | [`common/src/commonMain/.../views/call/CallView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt) | 28 | `expect fun ActiveCallView()`, delivery receipt waiting | +| `CallView.android.kt` | [`common/src/androidMain/.../views/call/CallView.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt) | 891 | Android WebView WebRTC, overlay, permissions | +| `CallView.desktop.kt` | [`common/src/desktopMain/.../views/call/CallView.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt) | 263 | Desktop browser WebRTC via NanoWSD | +| `CallActivity.kt` | [`android/src/main/java/.../views/call/CallActivity.kt`](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt) | 464 | Android call Activity, PiP, lock screen | +| `CallService.kt` | [`android/src/main/java/.../CallService.kt`](../../android/src/main/java/chat/simplex/app/CallService.kt) | 207 | Android foreground service for calls | +| `CallManager.kt` | [`common/src/commonMain/.../views/call/CallManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt) | 119 | Call lifecycle management | +| `WebRTC.kt` | [`common/src/commonMain/.../views/call/WebRTC.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt) | -- | `CallState` enum, `WCallCommand`, `WCallResponse` types | +| `IncomingCallAlertView.kt` | [`common/src/commonMain/.../views/call/IncomingCallAlertView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt) | 128 | In-app incoming call notification banner | +| `SimpleXAPI.kt` | [`common/src/commonMain/.../model/SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | -- | Call API commands (L1837--L1881) | diff --git a/apps/multiplatform/spec/services/files.md b/apps/multiplatform/spec/services/files.md new file mode 100644 index 0000000000..329e37dbb1 --- /dev/null +++ b/apps/multiplatform/spec/services/files.md @@ -0,0 +1,213 @@ +# File Transfer Service + +## Table of Contents + +1. [Overview](#1-overview) +2. [File Size Constants](#2-file-size-constants) +3. [CryptoFile](#3-cryptofile) +4. [File Storage Paths](#4-file-storage-paths) +5. [API Commands](#5-api-commands) +6. [Auto-Receive Logic](#6-auto-receive-logic) +7. [Source Files](#7-source-files) + +## Executive Summary + +SimpleX Chat uses two file transfer mechanisms: inline SMP transfers for small files (embedded in message bodies) and XFTP (eXtended File Transfer Protocol) for larger files up to 1 GB. Files are optionally encrypted at rest using `CryptoFile` functions backed by the chat core's native crypto library. File storage paths are platform-specific: Android uses `Context.dataDir`-based directories while Desktop uses platform-appropriate data directories (XDG on Linux, AppData on Windows, Application Support on macOS). Auto-receive logic automatically accepts images, voice messages, and videos below configurable size thresholds. + +--- + +## 1. Overview + +File transfer decision logic: + +- **Inline (SMP)**: Files small enough to be base64-encoded and embedded directly in an SMP message body. The practical limit is defined by `MAX_IMAGE_SIZE` (255 KB) for compressed images. The maximum SMP inline size is `MAX_FILE_SIZE_SMP` (~7.6 MB). +- **XFTP**: For files exceeding the inline threshold, up to `MAX_FILE_SIZE_XFTP` (1 GB). XFTP uses dedicated file relay servers with chunked, encrypted transfers. + +The `receiveFile` / `receiveFiles` API commands handle both protocols transparently -- the chat core selects the appropriate transfer mechanism based on file metadata received from the sender. + +--- + + + +## 2. File Size Constants + +Defined in [`Utils.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L118): + +| Constant | Value | Human-Readable | Line | Purpose | +|---|---|---|---|---| +| `MAX_IMAGE_SIZE` | 261,120 | 255 KB | [L118](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L118) | Inline image compression target | +| `MAX_IMAGE_SIZE_AUTO_RCV` | 522,240 | 510 KB | [L119](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L119) | Auto-receive threshold for images (`2 * MAX_IMAGE_SIZE`) | +| `MAX_VOICE_SIZE_AUTO_RCV` | 522,240 | 510 KB | [L120](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L120) | Auto-receive threshold for voice messages (`2 * MAX_IMAGE_SIZE`) | +| `MAX_VIDEO_SIZE_AUTO_RCV` | 1,047,552 | 1023 KB | [L121](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L121) | Auto-receive threshold for video | +| `MAX_VOICE_MILLIS_FOR_SENDING` | 300,000 | 5 min | [L123](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L123) | Maximum voice message duration | +| `MAX_FILE_SIZE_SMP` | 8,000,000 | ~7.6 MB | [L125](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L125) | Maximum SMP inline file size | +| `MAX_FILE_SIZE_XFTP` | 1,073,741,824 | 1 GB | [L127](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L127) | Maximum XFTP transfer size | +| `MAX_FILE_SIZE_LOCAL` | `Long.MAX_VALUE` | Unlimited | [L129](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L129) | Local file protocol (no size limit) | + +The `getMaxFileSize()` function ([`Utils.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt#L442)) selects the limit based on `FileProtocol`: + +```kotlin +FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP +FileProtocol.SMP -> MAX_FILE_SIZE_SMP +FileProtocol.LOCAL -> MAX_FILE_SIZE_LOCAL +``` + +--- + +## 3. CryptoFile + +[`CryptoFile.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt) (62 lines) + +Provides encrypted file I/O backed by the chat core's native cryptography (via JNI/JNA calls to `chatWriteFile`, `chatReadFile`, `chatEncryptFile`, `chatDecryptFile`). + +### Data types + +```kotlin +@Serializable +sealed class WriteFileResult { + @SerialName("result") data class Result(val cryptoArgs: CryptoFileArgs): WriteFileResult() + @SerialName("error") data class Error(val writeError: String): WriteFileResult() +} +``` + +`CryptoFileArgs` contains `fileKey` and `fileNonce` -- the symmetric encryption key and nonce for AES-GCM encryption. + + + +### Functions + +| Function | Line | Signature | Description | +|---|---|---|---| +| `writeCryptoFile` | [L24](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt#L24) | `(path: String, data: ByteArray): CryptoFileArgs` | Writes data to an encrypted file via a direct `ByteBuffer`. Returns the generated key and nonce. Requires initialized `ChatController`. | +| `readCryptoFile` | [L36](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt#L36) | `(path: String, cryptoArgs: CryptoFileArgs): ByteArray` | Reads and decrypts a file given its key and nonce. Returns the plaintext bytes. Throws on error (status != 0). | +| `encryptCryptoFile` | [L47](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt#L47) | `(fromPath: String, toPath: String): CryptoFileArgs` | Encrypts an existing plaintext file to a new encrypted file. Returns the generated key and nonce. | +| `decryptCryptoFile` | [L57](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt#L57) | `(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String)` | Decrypts an encrypted file to a plaintext output file. Throws on non-empty error string. | + +All functions delegate to native C library functions through the chat core JNI bridge. + +--- + + + +## 4. File Storage Paths + +### Common expect declarations + +[`Files.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) (191 lines, commonMain) + +| Property | Line | Description | +|---|---|---| +| `dataDir` | [L18](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L18) | Root application data directory | +| `tmpDir` | [L19](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L19) | Temporary files directory | +| `filesDir` | [L20](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L20) | Base files directory | +| `appFilesDir` | [L21](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L21) | Application files (chat attachments) | +| `wallpapersDir` | [L22](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L22) | Theme wallpaper images | +| `coreTmpDir` | [L23](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L23) | Temporary files for the chat core | +| `dbAbsolutePrefixPath` | [L24](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L24) | Database file path prefix | +| `preferencesDir` | [L25](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L25) | Preferences/config directory | +| `databaseExportDir` | [L35](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L35) | Temporary DB archive storage for export | +| `remoteHostsDir` | [L37](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L37) | Remote host connection data | + +### Android implementation + +[`Files.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt) (79 lines) + +| Property | Value | +|---|---| +| `dataDir` | `androidAppContext.dataDir` | +| `tmpDir` | `androidAppContext.getDir("temp", MODE_PRIVATE)` | +| `filesDir` | `dataDir/files` | +| `appFilesDir` | `dataDir/files/app_files` | +| `wallpapersDir` | `dataDir/files/assets/wallpapers` | +| `coreTmpDir` | `dataDir/files/temp_files` | +| `dbAbsolutePrefixPath` | `dataDir/files` | +| `preferencesDir` | `dataDir/shared_prefs` | +| `databaseExportDir` | `androidAppContext.cacheDir` | +| `remoteHostsDir` | `tmpDir/remote_hosts` | + +### Desktop implementation + +[`Files.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt) (116 lines) + +| Property | Value | +|---|---| +| `dataDir` | `desktopPlatform.dataPath` (XDG_DATA_HOME on Linux, AppData on Windows, Application Support on macOS) | +| `tmpDir` | `java.io.tmpdir/simplex` (deleted on exit) | +| `filesDir` | `dataDir/simplex_v1_files` | +| `appFilesDir` | Same as `filesDir` | +| `wallpapersDir` | `dataDir/simplex_v1_assets/wallpapers` | +| `coreTmpDir` | `dataDir/tmp` | +| `dbAbsolutePrefixPath` | `dataDir/simplex_v1` | +| `preferencesDir` | `desktopPlatform.configPath` | +| `databaseExportDir` | Same as `tmpDir` | +| `remoteHostsDir` | `dataDir/remote_hosts` | + +### Helper functions (common) + +| Function | Line | Description | +|---|---|---| +| `getAppFilePath` | [L81](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L81) | Resolves file path considering remote hosts | +| `getWallpaperFilePath` | [L91](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L91) | Resolves wallpaper image path, creates parent directories | +| `getLoadedFilePath` | [L105](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L105) | Returns path if file exists and is fully loaded | +| `getLoadedFileSource` | [L115](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L115) | Returns `CryptoFile` source if file is loaded | +| `readThemeOverrides` | [L125](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L125) | Reads theme overrides from `themes.yaml` | +| `writeThemeOverrides` | [L151](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L151) | Atomically writes theme overrides to `themes.yaml` | +| `copyFileToFile` | [L47](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L47) | Copies a `File` to a `URI` destination with toast feedback | +| `copyBytesToFile` | [L63](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L63) | Copies a `ByteArrayInputStream` to a `URI` destination | + +--- + +## 5. API Commands + +Defined in [`SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt): + +| Function | Line | Signature | Description | +|---|---|---|---| +| `receiveFiles` | [L1946](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1946) | `(rhId, user, fileIds, userApprovedRelays, auto)` | Receive multiple files. Sends `CC.ReceiveFile` for each ID. Handles relay approval workflow: collects unapproved files, shows alert, re-calls with `userApprovedRelays=true`. Respects `privacyEncryptLocalFiles` preference. | +| `receiveFile` | [L2062](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2062) | `(rhId, user, fileId, userApprovedRelays, auto)` | Delegates to `receiveFiles` with a single-element list. | +| `cancelFile` | [L2072](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2072) | `(rh, user, fileId)` | Cancels an in-progress file transfer (send or receive). Cleans up the local file. | +| `apiCancelFile` | [L2080](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2080) | `(rh, fileId, ctrl?)` | Low-level cancel. Returns `AChatItem?` on success (`SndFileCancelled` or `RcvFileCancelled`). | +| `uploadStandaloneFile` | [L1916](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1916) | `(user, file, ctrl?)` | Upload a standalone file (for database migration). Returns `FileTransferMeta?` with XFTP link. | +| `downloadStandaloneFile` | [L1926](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L1926) | `(user, url, file, ctrl?)` | Download a standalone file from an XFTP URL. Returns `RcvFileTransfer?`. | + +--- + +## 6. Auto-Receive Logic + +Located in [`SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L2696) within the `CR.NewChatItems` handler: + +```kotlin +if (file != null && + appPrefs.privacyAcceptImages.get() && + ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) + || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) + || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV + && file.fileStatus !is CIFileStatus.RcvAccepted)) +) { + receiveFile(rhId, r.user, file.fileId, auto = true) +} +``` + +**Conditions for auto-receive:** + +1. The `privacyAcceptImages` preference is enabled (user opt-in). +2. The content type and size match one of: + - **Images** (`MCImage`): file size <= 510 KB (`MAX_IMAGE_SIZE_AUTO_RCV`) + - **Video** (`MCVideo`): file size <= 1023 KB (`MAX_VIDEO_SIZE_AUTO_RCV`) + - **Voice** (`MCVoice`): file size <= 510 KB (`MAX_VOICE_SIZE_AUTO_RCV`) AND file is not already accepted +3. The file has a non-null `file` attachment. + +When `auto = true`, relay approval alerts are suppressed (the file is silently received). + +--- + +## 7. Source Files + +| File | Path | Lines | Description | +|---|---|---|---| +| `CryptoFile.kt` | [`common/src/commonMain/.../model/CryptoFile.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt) | 62 | Encrypted file read/write via native crypto | +| `Files.kt` | [`common/src/commonMain/.../platform/Files.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) | 191 | Common file path declarations, theme I/O, file helpers | +| `Files.android.kt` | [`common/src/androidMain/.../platform/Files.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt) | 79 | Android file path implementations | +| `Files.desktop.kt` | [`common/src/desktopMain/.../platform/Files.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt) | 116 | Desktop file path implementations | +| `Utils.kt` | [`common/src/commonMain/.../views/helpers/Utils.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt) | -- | File size constants (L117--L128), `getMaxFileSize()` (L442) | +| `SimpleXAPI.kt` | [`common/src/commonMain/.../model/SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | -- | File transfer API commands (L1911--L2085), auto-receive (L2690) | diff --git a/apps/multiplatform/spec/services/notifications.md b/apps/multiplatform/spec/services/notifications.md new file mode 100644 index 0000000000..6ce4bc9dc1 --- /dev/null +++ b/apps/multiplatform/spec/services/notifications.md @@ -0,0 +1,261 @@ +# Notification System + +## Table of Contents + +1. [Overview](#1-overview) +2. [NtfManager Abstract Class](#2-ntfmanager-abstract-class) +3. [Android Notification Manager](#3-android-notification-manager) +4. [Desktop Notification Manager](#4-desktop-notification-manager) +5. [Android Background Messaging](#5-android-background-messaging) +6. [Notification Privacy](#6-notification-privacy) +7. [Source Files](#7-source-files) + +## Executive Summary + +SimpleX Chat uses platform-specific notification strategies. The common `NtfManager` abstract class defines the notification contract with shared helper methods for message, contact, and call notifications. Android implements a full notification system with channels, grouped summaries, full-screen call intents, and a foreground service (`SimplexService`) or periodic `WorkManager` tasks for background message fetching. Desktop uses the TwoSlices library (with OS-native fallbacks) for system notifications. Notification privacy is controlled via `NotificationPreviewMode` (MESSAGE, CONTACT, HIDDEN). + +--- + +## 1. Overview + +Notifications serve three purposes in SimpleX Chat: + +1. **Message notifications** -- alert users to new messages when the app is not focused on the relevant chat. +2. **Call notifications** -- high-priority alerts for incoming WebRTC calls, with full-screen intent support on Android for lock-screen scenarios. +3. **Contact events** -- notifications for contact connection and contact request events. + +The architecture uses an abstract `NtfManager` in common code with platform-specific `actual` implementations. On Android, background message delivery requires a foreground service or periodic WorkManager tasks since SimpleX does not use push notifications (no Firebase/APNs dependency for privacy). + +--- + + + + +## 2. NtfManager Abstract Class + +[`NtfManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt) (139 lines, commonMain) + +The global `ntfManager` instance is declared at [line 17](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L17) and initialized by each platform at startup. + +### Concrete methods + +| Method | Line | Description | +|---|---|---| +| `notifyContactConnected` | [L20](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L20) | Displays "contact connected" notification for a `Contact` | +| `notifyContactRequestReceived` | [L27](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L27) | Shows contact request notification with an "Accept" action button | +| `notifyMessageReceived` | [L38](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L38) | Conditionally shows message notification based on `ntfsEnabled`, `showNotification`, and whether user is viewing that chat | +| `acceptContactRequestAction` | [L51](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L51) | Accepts a contact request from a notification action | +| `openChatAction` | [L59](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L59) | Opens a specific chat from a notification tap, switching user if needed | +| `showChatsAction` | [L74](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L74) | Opens the chat list, switching user if needed | +| `acceptCallAction` | [L88](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L88) | Accepts a call invitation from a notification action | + +### Abstract methods + +| Method | Line | Description | +|---|---|---| +| `notifyCallInvitation` | [L98](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L98) | Show call notification; returns `true` if notification was shown | +| `displayNotification` | [L102](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L102) | Display a message notification with optional image and action buttons | +| `cancelCallNotification` | [L103](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L103) | Cancel the active call notification | +| `hasNotificationsForChat` | [L99](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L99) | Check if notifications exist for a given chat | +| `cancelNotificationsForChat` | [L100](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L100) | Cancel all notifications for a specific chat | +| `cancelNotificationsForUser` | [L101](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L101) | Cancel all notifications for a user profile | +| `cancelAllNotifications` | [L104](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L104) | Cancel all notifications | +| `showMessage` | [L105](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L105) | Show a simple title+text notification | +| `androidCreateNtfChannelsMaybeShowAlert` | [L107](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L107) | Android-only: create notification channels (triggers permission prompt on Android 13+) | + +### Private helpers + +- `awaitChatStartedIfNeeded` ([line 109](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L109)): Waits up to 30 seconds for chat initialization (handles database decryption delay). +- `hideSecrets` ([line 122](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L122)): Replaces `Format.Secret` formatted text with `"..."` in notification previews. + +--- + +## 3. Android Notification Manager + +[`NtfManager.android.kt`](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt) (331 lines) + +Implemented as a Kotlin `object` (singleton) in the Android module. + +### Notification channels + +| Channel | Constant | Importance | Purpose | +|---|---|---|---| +| Messages | `MessageChannel` (`chat.simplex.app.MESSAGE_NOTIFICATION`) | HIGH | All chat message notifications | +| Calls | `CallChannel` (`chat.simplex.app.CALL_NOTIFICATION_2`) | HIGH | Incoming call alerts with custom ringtone and vibration | + +Channel creation happens in `createNtfChannelsMaybeShowAlert()` ([line 298](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L298)). Old channel IDs (`CALL_NOTIFICATION`, `CALL_NOTIFICATION_1`, `LOCK_SCREEN_CALL_NOTIFICATION`) are explicitly deleted. + +### displayNotification (messages) + +[Line 102](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L102): + +- Uses `NotificationCompat.Builder` with `MessageChannel`. +- Groups notifications using `MessageGroup` with `GROUP_ALERT_CHILDREN` behavior. +- Applies rate limiting: silent mode if notification for the same `(userId, chatId)` was shown within 30 seconds (`msgNtfTimeoutMs`). +- Creates a group summary notification ([line 142](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L142)) with `setGroupSummary(true)`. +- Content intent uses `TaskStackBuilder` for proper back stack. +- Supports `NotificationAction.ACCEPT_CONTACT_REQUEST` action buttons via `NtfActionReceiver` broadcast receiver. + +### notifyCallInvitation + +[Line 160](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L160): + +- Returns `false` (no notification) if app is in foreground -- in-app alert is used instead. +- **Lock screen / screen off**: Uses `setFullScreenIntent` with a `PendingIntent` to `CallActivity`, plus `VISIBILITY_PUBLIC`. +- **Foreground / unlocked**: Uses regular notification with Accept/Reject action buttons and a custom ringtone (`ring_once` raw resource). +- Notification flags include `FLAG_INSISTENT` for repeating sound and vibration. +- Call notification channel vibration pattern: `[250, 250, 0, 2600]` ms. + +### Cancel operations + +| Method | Line | Description | +|---|---|---| +| `cancelNotificationsForChat` | [L75](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L75) | Cancels by `chatId.hashCode()`, cleans up group summary if no children remain | +| `cancelNotificationsForUser` | [L88](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L88) | Iterates and cancels all notifications for a given `userId` | +| `cancelCallNotification` | [L261](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L261) | Cancels the singleton call notification (`CallNotificationId = -1`) | +| `cancelAllNotifications` | [L265](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L265) | Cancels all via `NotificationManager.cancelAll()` | + +### NtfActionReceiver + +[Line 311](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt#L311): A `BroadcastReceiver` that handles notification action intents: +- `ACCEPT_CONTACT_REQUEST` -- calls `ntfManager.acceptContactRequestAction()` +- `RejectCallAction` -- calls `callManager.endCall()` on the invitation + +--- + +## 4. Desktop Notification Manager + +[`NtfManager.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt) (193 lines) + +Implemented as a Kotlin `object` using the [TwoSlices](https://github.com/sshtools/two-slices) library (`Toast` builder API) for cross-platform desktop notifications. + +### displayNotification + +[Line 97](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L97): + +- Suppresses if `!user.showNotifications`. +- Respects `NotificationPreviewMode` for title and content. +- Calls `displayNotificationViaLib()` ([line 114](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L114)) which builds a `Toast` with title, content, icon, action buttons, and default action. +- Icon images are written to a temporary PNG file via `prepareIconPath()` ([line 150](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L150)). +- Default action on click opens the relevant chat via `openChatAction()`. + +### notifyCallInvitation + +[Line 22](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L22): + +- Returns `false` if the SimpleX window is focused (in-app alert used instead). +- Creates a notification with Accept and Reject action buttons. +- Default click action opens the chat. + +### OS-native fallbacks + +[Line 162](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt#L162): The `displayNotification` private method dispatches based on `desktopPlatform`: + +| Platform | Method | +|---|---| +| Linux | `notify-send` command with optional `-i` icon | +| Windows | `SystemTray` with `TrayIcon.displayMessage()` | +| macOS | `osascript -e 'display notification ...'` | + +### Notification tracking + +Previous notifications are tracked in `prevNtfs: ArrayList, Slice>>` with a `Mutex` for thread safety. Cancel operations remove entries from this list. + +--- + +## 5. Android Background Messaging + +### 5.1 SimplexService.kt (734 lines) + +[`SimplexService.kt`](../../android/src/main/java/chat/simplex/app/SimplexService.kt) + +A foreground `Service` that keeps the app process alive for continuous message receiving. This is SimpleX's privacy-preserving alternative to push notifications. + +**Service lifecycle:** + +- `startService()` ([line 128](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L128)): Waits for database migration, validates DB status, saves service state as STARTED. WakeLock acquisition is commented out -- the app relies on battery optimization whitelisting instead. +- `onDestroy()` ([line 87](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L87)): Releases wakelocks, saves state as STOPPED, sends broadcast to `AutoRestartReceiver` if allowed. +- `onTaskRemoved()` ([line 211](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L211)): Schedules restart via `AlarmManager` when the app is swiped from recents. + +**Notification:** + +- Channel: `SIMPLEX_SERVICE_NOTIFICATION` with `IMPORTANCE_LOW` and badge disabled ([line 165](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L165)). +- Shows a persistent notification with a "Hide notification" action that opens channel settings. +- Service ID: `6789`. + +**Restart mechanisms:** + +| Receiver | Line | Trigger | +|---|---|---| +| `StartReceiver` | [L234](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L234) | Device boot (`BOOT_COMPLETED`) | +| `AutoRestartReceiver` | [L253](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L253) | Service destruction | +| `AppUpdateReceiver` | [L261](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L261) | App update (`MY_PACKAGE_REPLACED`) | +| `ServiceStartWorker` | [L283](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L283) | WorkManager one-time task | + +**Battery optimization:** + +- `isBackgroundAllowed()` ([line 681](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L681)): Checks both `isIgnoringBatteryOptimizations` and `!isBackgroundRestricted`. +- `showBackgroundServiceNoticeIfNeeded()` ([line 430](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L430)): Shows alerts guiding users to disable battery optimization or background restriction. Includes Xiaomi-specific guidance. +- `disableNotifications()` ([line 722](../../android/src/main/java/chat/simplex/app/SimplexService.kt#L722)): Switches mode to OFF, disables receivers, cancels workers. + +### 5.2 MessagesFetcherWorker.kt (100 lines) + +[`MessagesFetcherWorker.kt`](../../android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt) + +A `CoroutineWorker` used in `PERIODIC` notification mode as an alternative to the persistent foreground service: + +- `scheduleWork()` ([line 18](../../android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt#L18)): Schedules a `OneTimeWorkRequest` with a default 600-second (10 minute) initial delay and 60-second duration. Requires `NetworkType.CONNECTED` constraint. +- `doWork()` ([line 53](../../android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt#L53)): Skips if `SimplexService` is already running. Initializes chat controller if needed (self-destruct mode). Waits for DB migration. Runs for up to `durationSec` seconds, polling every 5 seconds until no messages have been received for 10 seconds (`WAIT_AFTER_LAST_MESSAGE`). +- Self-rescheduling: Always calls `reschedule()` at the end (creating a chain of one-time tasks that simulate periodic execution). + + + +### 5.3 Notification modes + +Defined in [`SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L7739): + +```kotlin +enum class NotificationsMode { + OFF, // No background message fetching + PERIODIC, // WorkManager periodic tasks (MessagesFetcherWorker) + SERVICE; // Persistent foreground service (SimplexService) +} +``` + +Default is `SERVICE`. The `requiresIgnoringBattery` property is an Android extension property (defined in `Extensions.kt`, not on the enum itself) whose value depends on the SDK version: `SERVICE` requires ignoring battery optimizations since SDK S (API 31), `PERIODIC` since SDK M (API 23). + +--- + +## 6. Notification Privacy + +Defined in [`ChatModel.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L4823): + +```kotlin +enum class NotificationPreviewMode { + MESSAGE, // Show sender name and message text + CONTACT, // Show sender name, generic "new message" text + HIDDEN; // Show "Somebody" as sender, generic "new message" text +} +``` + +Privacy mode affects: +- **Notification title**: `HIDDEN` uses `"Somebody"` instead of contact name. +- **Notification content**: Only `MESSAGE` mode shows actual message text. +- **Large icon**: `HIDDEN` uses the app icon instead of the contact's profile image. +- **Call notifications**: `HIDDEN` hides the caller's name and profile image. + +Both Android and Desktop implementations check `appPreferences.notificationPreviewMode.get()` before constructing notification content. + +--- + +## 7. Source Files + +| File | Path | Lines | Description | +|---|---|---|---| +| `NtfManager.kt` | [`common/src/commonMain/.../platform/NtfManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt) | 139 | Abstract notification manager with shared logic | +| `NtfManager.android.kt` | [`android/src/main/java/.../model/NtfManager.android.kt`](../../android/src/main/java/chat/simplex/app/model/NtfManager.android.kt) | 331 | Android notification channels, groups, call intents | +| `NtfManager.desktop.kt` | [`common/src/desktopMain/.../model/NtfManager.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt) | 193 | Desktop notifications via TwoSlices/OS-native | +| `SimplexService.kt` | [`android/src/main/java/.../SimplexService.kt`](../../android/src/main/java/chat/simplex/app/SimplexService.kt) | 734 | Android foreground service for background messaging | +| `MessagesFetcherWorker.kt` | [`android/src/main/java/.../MessagesFetcherWorker.kt`](../../android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt) | 100 | WorkManager periodic message fetcher | +| `ChatModel.kt` | [`common/src/commonMain/.../model/ChatModel.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt) | -- | `NotificationPreviewMode` enum (L4823) | +| `SimpleXAPI.kt` | [`common/src/commonMain/.../model/SimpleXAPI.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | -- | `NotificationsMode` enum (L7739) | diff --git a/apps/multiplatform/spec/services/theme.md b/apps/multiplatform/spec/services/theme.md new file mode 100644 index 0000000000..e5839fc193 --- /dev/null +++ b/apps/multiplatform/spec/services/theme.md @@ -0,0 +1,498 @@ +# Theme Engine + +## Table of Contents + +1. [Overview](#1-overview) +2. [ThemeManager](#2-thememanager) +3. [Default Themes](#3-default-themes) +4. [Theme Types](#4-theme-types) +5. [Color System](#5-color-system) +6. [SimpleXTheme Composable](#6-simplextheme-composable) +7. [Platform Theme](#7-platform-theme) +8. [YAML Import/Export](#8-yaml-importexport) +9. [Source Files](#9-source-files) + +## Executive Summary + +The SimpleX Chat theme engine implements a four-level cascade: per-chat theme overrides take precedence over per-user overrides, which take precedence over global (app-settings) overrides, which take precedence over built-in presets. Four preset themes exist (LIGHT, DARK, SIMPLEX, BLACK), each defining a Material `Colors` palette and custom `AppColors` for chat-specific elements. Themes support wallpaper customization (preset patterns or custom images) with background and tint color overrides. Theme configuration is persisted as YAML and can be imported/exported. The `SimpleXTheme` composable wraps `MaterialTheme` with additional `CompositionLocal` providers for app colors and wallpaper. + +--- + +## 1. Overview + +Theme resolution follows a priority chain: + +``` +per-chat override > per-user override > global override > preset default +``` + +At each level, individual color properties can be overridden. Unspecified properties fall through to the next level. The resolution is performed by `ThemeManager.currentColors()`, which merges all levels into a single `ActiveTheme` containing Material `Colors`, `AppColors`, and `AppWallpaper`. + +Wallpapers follow the same cascade, with additional support for preset wallpapers (built-in patterns like `SCHOOL`) and custom images. Wallpaper presets can define their own color overrides that sit between the global override and the base preset. + +--- + +## 2. ThemeManager + +[`ThemeManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt) (241 lines) + +A singleton `object` that manages theme state, persistence, and resolution. + +### Core resolution + + + +**`currentColors()`** ([line 57](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L57)): + +```kotlin +fun currentColors( + themeOverridesForType: WallpaperType?, + perChatTheme: ThemeModeOverride?, + perUserTheme: ThemeModeOverrides?, + appSettingsTheme: List +): ActiveTheme +``` + +This is the core resolution function. It: +1. Determines the non-system theme name (resolving `SYSTEM` to light or dark based on `systemInDarkThemeCurrently`). +2. Selects the base theme palette (LIGHT/DARK/SIMPLEX/BLACK). +3. Finds the matching `ThemeOverrides` from `appSettingsTheme` based on wallpaper type and theme name. +4. Selects the `perUserTheme` for the current light/dark mode. +5. Resolves wallpaper preset colors if applicable. +6. Merges all color layers via `toColors()`, `toAppColors()`, and `toAppWallpaper()`. + +Returns `ActiveTheme(name, base, colors, appColors, wallpaper)`. + +### Theme application + +**`applyTheme()`** ([line 105](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L105)): + +Persists the theme name, recalculates `CurrentColors`, and updates Android system bar appearance: + +```kotlin +fun applyTheme(theme: String) { + if (appPrefs.currentTheme.get() != theme) { + appPrefs.currentTheme.set(theme) + } + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + platform.androidSetNightModeIfSupported() + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) +} +``` + +**`changeDarkTheme()`** ([line 115](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L115)): + +Sets the dark mode variant (DARK, SIMPLEX, or BLACK) and recalculates colors. + +### Color and wallpaper modification + +**`saveAndApplyThemeColor()`** ([line 120](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L120)): + +Persists a single color change to the global theme overrides: +1. Gets or creates `ThemeOverrides` for the current base theme. +2. Calls `withUpdatedColor()` to update the specific `ThemeColor`. +3. Updates `currentThemeIds` mapping. +4. Recalculates `CurrentColors`. + +**`applyThemeColor()`** ([line 132](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L132)): + +In-memory-only color change (for per-chat/per-user theme editing before save). + +**`saveAndApplyWallpaper()`** ([line 136](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L136)): + +Persists wallpaper type change. Finds or creates matching `ThemeOverrides` (matching by wallpaper type + theme name), updates the wallpaper, and persists. + +### Reset + +**`resetAllThemeColors()` (global)** ([line 204](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L204)): + +Resets all custom colors in the current global theme override to defaults. Preserves wallpaper but clears its background and tint overrides. + +**`resetAllThemeColors()` (per-chat/per-user)** ([line 213](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L213)): + +In-memory reset of a `ThemeModeOverride` state. + +### Import/Export + +**`saveAndApplyThemeOverrides()`** ([line 188](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L188)): + +Imports a complete `ThemeOverrides` (from YAML). Handles wallpaper image import (base64 to file), replaces existing override for the same type, and applies. + +**`currentThemeOverridesForExport()`** ([line 92](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L92)): + +Exports the fully resolved current theme as a `ThemeOverrides` with all colors filled and wallpaper image embedded as base64. + +### Utility + +**`colorFromReadableHex()`** ([line 224](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L224)): + +Parses `#AARRGGBB` hex string to `Color`. + +**`toReadableHex()`** ([line 227](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L227)): + +Converts `Color` to `#AARRGGBB` hex string with intelligent alpha handling. + +--- + + + +## 3. Default Themes + +[`Theme.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L26): + +```kotlin +enum class DefaultTheme { + LIGHT, DARK, SIMPLEX, BLACK; + + companion object { + const val SYSTEM_THEME_NAME: String = "SYSTEM" + } +} +``` + +| Theme | `mode` | Description | +|---|---|---| +| `LIGHT` | LIGHT | Standard light theme with white/light gray surfaces | +| `DARK` | DARK | Standard dark theme with dark gray surfaces | +| `SIMPLEX` | DARK | SimpleX branded dark theme with deep blue background and cyan accent | +| `BLACK` | DARK | AMOLED-optimized pure black theme | + +`SYSTEM` is a virtual theme name that resolves to LIGHT or the configured dark variant at runtime. + +`DefaultThemeMode` ([line 46](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L46)): `LIGHT` or `DARK`, serialized as `"light"` / `"dark"`. + +--- + +## 4. Theme Types + + + +### AppColors (line 53) + +[`Theme.kt` L53](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L53): + +```kotlin +@Stable +class AppColors( + title: Color, + primaryVariant2: Color, + sentMessage: Color, + sentQuote: Color, + receivedMessage: Color, + receivedQuote: Color, +) +``` + +Mutable state properties (for efficient recomposition) representing chat-specific colors not covered by Material's `Colors`. + + + +### AppWallpaper (line 106) + +[`Theme.kt` L106](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L106): + +```kotlin +@Stable +class AppWallpaper( + background: Color? = null, + tint: Color? = null, + type: WallpaperType = WallpaperType.Empty, +) +``` + +Represents the active wallpaper state with optional background color, tint overlay, and wallpaper type (Empty, Preset, or Image). + + + +### ThemeColor (line 140) + +Enum of all customizable color slots: + +`PRIMARY`, `PRIMARY_VARIANT`, `SECONDARY`, `SECONDARY_VARIANT`, `BACKGROUND`, `SURFACE`, `TITLE`, `SENT_MESSAGE`, `SENT_QUOTE`, `RECEIVED_MESSAGE`, `RECEIVED_QUOTE`, `PRIMARY_VARIANT2`, `WALLPAPER_BACKGROUND`, `WALLPAPER_TINT` + +Each has a `fromColors()` method to extract the current value and a `text` property for UI display. + + + +### ThemeColors (line 183) + +[`Theme.kt` L183](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L183): + +Serializable data class with optional hex color strings for each slot. Uses `@SerialName` annotations for YAML compatibility (`accent` for `primary`, `accentVariant` for `primaryVariant`, `menus` for `surface`, etc.). + + + +### ThemeWallpaper (line 224) + +[`Theme.kt` L224](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L224): + +```kotlin +@Serializable +data class ThemeWallpaper( + val preset: String? = null, // Preset wallpaper name + val scale: Float? = null, // Wallpaper scale factor + val scaleType: WallpaperScaleType? = null, // Fill/fit mode + val background: String? = null, // Background color hex + val tint: String? = null, // Tint overlay color hex + val image: String? = null, // Base64-encoded image (for import/export) + val imageFile: String? = null, // Local image file name +) +``` + +Key methods: +- `toAppWallpaper()`: Converts to runtime `AppWallpaper`. +- `withFilledWallpaperBase64()`: Embeds the image as base64 for export. +- `importFromString()`: Saves a base64 image to disk and returns a copy with `imageFile` set. +- `from(type, background, tint)`: Factory from `WallpaperType`. + + + +### ThemeOverrides (line 304) + +[`Theme.kt` L304](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L304): + +```kotlin +@Serializable +data class ThemeOverrides( + val themeId: String = UUID.randomUUID().toString(), + val base: DefaultTheme, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) +``` + +A complete theme override entry. Multiple can coexist (one per wallpaper type per base theme). The `themeId` is a UUID for identity tracking. Key methods: +- `isSame(type, themeName)`: Matches by wallpaper type and base theme. +- `withUpdatedColor(name, color)`: Returns a copy with one color changed. +- `toColors()`, `toAppColors()`, `toAppWallpaper()`: Merge with base theme and per-user/per-chat overrides. + + + +### ThemeModeOverrides (line 475) + +[`Theme.kt` L475](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L475): + +```kotlin +@Serializable +data class ThemeModeOverrides( + val light: ThemeModeOverride? = null, + val dark: ThemeModeOverride? = null, +) +``` + +Container for per-user or per-chat overrides, with separate light and dark mode variants. Stored on the `User` model as `uiThemes`. + + + +### ThemeModeOverride (line 487) + +[`Theme.kt` L487](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L487): + +```kotlin +@Serializable +data class ThemeModeOverride( + val mode: DefaultThemeMode = CurrentColors.value.base.mode, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) +``` + +A single mode's override with colors and wallpaper. Has `withUpdatedColor()` and `removeSameColors()` (strips colors that match base defaults). + +--- + +## 5. Color System + +Four built-in color palettes, each consisting of a Material `Colors` and an `AppColors`: + +### DarkColorPalette ([line 634](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L634)) + +| Property | Value | Notes | +|---|---|---| +| `primary` | `SimplexBlue` | `#0088ff` | +| `surface` | `#222222` | | +| `sentMessage` | `#18262E` | Dark blue-gray | +| `receivedMessage` | `#262627` | Neutral dark | + +### LightColorPalette ([line 656](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L656)) + +| Property | Value | Notes | +|---|---|---| +| `primary` | `SimplexBlue` | `#0088ff` | +| `surface` | `White` | | +| `sentMessage` | `#E9F7FF` | Light blue | +| `receivedMessage` | `#F5F5F6` | Near-white | + +### SimplexColorPalette ([line 678](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L678)) + +| Property | Value | Notes | +|---|---|---| +| `primary` | `#70F0F9` | Cyan | +| `primaryVariant` | `#1298A5` | Dark cyan | +| `background` | `#111528` | Deep navy | +| `surface` | `#121C37` | Dark navy | +| `title` | `#267BE5` | Blue | + +### BlackColorPalette ([line 701](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L701)) + +| Property | Value | Notes | +|---|---|---| +| `primary` | `#0077E0` | Darker blue | +| `background` | `#070707` | Near-black | +| `surface` | `#161617` | Very dark | +| `sentMessage` | `#18262E` | Same as Dark | +| `receivedMessage` | `#1B1B1B` | Very dark | + +--- + + + +## 6. SimpleXTheme Composable + +[`Theme.kt` line 773](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L773): + +```kotlin +@Composable +fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) +``` + +The root theme composable that wraps all app content: + +1. **System dark mode tracking** ([line 781](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L781)): Uses `snapshotFlow` on `isSystemInDarkTheme()` to call `reactOnDarkThemeChanges()` when the system theme changes. This triggers `ThemeManager.applyTheme(SYSTEM)` if the app is in system theme mode. + +2. **User theme tracking** ([line 790](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L790)): Monitors `chatModel.currentUser.value?.uiThemes` and re-applies the theme when the active user changes. + +3. **MaterialTheme wrapping** ([line 797](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L797)): Provides `theme.colors` to `MaterialTheme`, plus custom `CompositionLocal` providers: + - `LocalContentColor` -- set to `MaterialTheme.colors.onBackground` + - `LocalAppColors` -- the `AppColors` instance (remembered and updated) + - `LocalAppWallpaper` -- the `AppWallpaper` instance (remembered and updated) + - `LocalDensity` -- scaled by `desktopDensityScaleMultiplier` and `fontSizeMultiplier` + +4. **`SimpleXThemeOverride`** ([line 825](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L825)): A variant that accepts an explicit `ActiveTheme` for per-chat theme previews and overlays. + +### CompositionLocal access + +```kotlin +val MaterialTheme.appColors: AppColors // via LocalAppColors +val MaterialTheme.wallpaper: AppWallpaper // via LocalAppWallpaper +``` + +### Global state + + + +`CurrentColors` ([line 727](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L727)): A `MutableStateFlow` that holds the current resolved theme. Updated by `ThemeManager.applyTheme()` and collected by `SimpleXTheme`. + +`systemInDarkThemeCurrently` ([line 724](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L724)): Tracks the current system dark mode state. + +--- + +## 7. Platform Theme + +### isSystemInDarkTheme + +**Android** ([`Theme.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt)): + +```kotlin +@Composable +actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme() +``` + +Delegates to the standard Compose function which reads `Configuration.uiMode`. + +**Desktop** ([`Theme.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt)): + +```kotlin +private val detector: OsThemeDetector = OsThemeDetector.getDetector() + .apply { registerListener(::reactOnDarkThemeChanges) } + +@Composable +actual fun isSystemInDarkTheme(): Boolean = try { + detector.isDark +} catch (e: Exception) { + false // Fallback for macOS exceptions +} +``` + +Uses the [jSystemThemeDetector](https://github.com/Dansoftowner/jSystemThemeDetector) library (`OsThemeDetector`). The detector also registers a listener that calls `reactOnDarkThemeChanges()` proactively when the OS theme changes, ensuring the app responds even outside of composition. + +### reactOnDarkThemeChanges + +[`Theme.kt` line 763](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L763): + +```kotlin +fun reactOnDarkThemeChanges(isDark: Boolean) { + systemInDarkThemeCurrently = isDark + if (appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME + && CurrentColors.value.colors.isLight == isDark) { + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + } +} +``` + +Only triggers a theme switch if the app is in SYSTEM mode and the current light/dark state disagrees with the OS. + +--- + +## 8. YAML Import/Export + +Theme overrides are persisted in `themes.yaml` (located in `preferencesDir`). + +### readThemeOverrides + +[`Files.kt` line 125](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L125): + +```kotlin +fun readThemeOverrides(): List +``` + +1. Reads `themes.yaml` from `preferencesDir`. +2. Parses the YAML node tree. +3. Extracts the `themes` list. +4. Deserializes each entry as `ThemeOverrides`, skipping entries that fail to parse (with error logging). +5. Calls `skipDuplicates()` to remove entries with the same type+base combination. + +### writeThemeOverrides + +[`Files.kt` line 151](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L151): + +```kotlin +fun writeThemeOverrides(overrides: List): Boolean +``` + +1. Serializes `ThemesFile(themes = overrides)` to YAML string. +2. Writes to a temporary file in `preferencesTmpDir`. +3. Atomically moves the temp file to `themes.yaml` using `Files.move` with `REPLACE_EXISTING`. +4. Thread-safe via `synchronized(lock)`. + +### YAML format + +```yaml +themes: + - themeId: "uuid-string" + base: "LIGHT" + colors: + accent: "#ff0088ff" + background: "#ffffffff" + sentMessage: "#ffe9f7ff" + wallpaper: + preset: "school" + scale: 1.0 + background: "#ccffffff" + tint: "#22000000" +``` + +Uses the [kaml](https://github.com/charleskorn/kaml) YAML library for serialization. `ThemeColors` uses `@SerialName` annotations for cross-platform YAML key compatibility (e.g., `accent` for `primary`, `menus` for `surface`). + +--- + +## 9. Source Files + +| File | Path | Lines | Description | +|---|---|---|---| +| `ThemeManager.kt` | [`common/src/commonMain/.../ui/theme/ThemeManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt) | 241 | Theme resolution, persistence, color/wallpaper management | +| `Theme.kt` | [`common/src/commonMain/.../ui/theme/Theme.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt) | 848 | Type definitions, color palettes, `SimpleXTheme` composable | +| `Theme.android.kt` | [`common/src/androidMain/.../ui/theme/Theme.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt) | 6 | Android `isSystemInDarkTheme` | +| `Theme.desktop.kt` | [`common/src/desktopMain/.../ui/theme/Theme.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt) | 25 | Desktop `isSystemInDarkTheme` via OsThemeDetector | +| `Files.kt` | [`common/src/commonMain/.../platform/Files.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) | 191 | `readThemeOverrides()` (L125), `writeThemeOverrides()` (L151) | diff --git a/apps/multiplatform/spec/state.md b/apps/multiplatform/spec/state.md new file mode 100644 index 0000000000..900d6593ab --- /dev/null +++ b/apps/multiplatform/spec/state.md @@ -0,0 +1,486 @@ +# State Management + +## Table of Contents + +1. [Overview](#1-overview) +2. [ChatModel](#2-chatmodel) +3. [ChatsContext](#3-chatscontext) +4. [Chat](#4-chat) +5. [AppPreferences](#5-apppreferences) +6. [Source Files](#6-source-files) + +--- + +## 1. Overview + +SimpleX Chat uses a **singleton-based, Compose-reactive state model**. The primary state holder is `ChatModel`, a Kotlin `object` annotated with `@Stable`. All mutable fields are Compose `MutableState`, `MutableStateFlow`, or `SnapshotStateList`/`SnapshotStateMap` instances, which trigger Compose recomposition on mutation. + +There is no ViewModel layer, no dependency injection framework, and no Redux/MVI pattern. The architecture is: + +``` +ChatModel (singleton, global Compose state) + | + +-- ChatController (command dispatch + event processing) + | | + | +-- sendCmd() -> chatSendCmdRetry() [JNI] + | +-- recvMsg() -> chatRecvMsgWait() [JNI] + | +-- processReceivedMsg() -> mutates ChatModel fields + | + +-- AppPreferences (150+ SharedPreferences via multiplatform-settings) + | + +-- ChatsContext (primary) -- chat list + current chat items + +-- ChatsContext? (secondary) -- optional second context for dual-pane/support chat +``` + +State mutations originate from two sources: +1. **User actions**: Compose UI handlers call `api*()` suspend functions on `ChatController`, which send commands to the Haskell core, receive responses, and update `ChatModel`. +2. **Core events**: The receiver coroutine (`startReceiver`) calls `processReceivedMsg()`, which updates `ChatModel` fields on `Dispatchers.Main`. + +--- + + + +## 2. ChatModel + +Defined at [`ChatModel.kt line 86`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L86) as `@Stable object ChatModel`. + +### Controller Reference + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`controller`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L87) | `ChatController` | 87 | Reference to the `ChatController` singleton | + +### User State + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`currentUser`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L89) | `MutableState` | 89 | Currently active user profile | +| [`users`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L90) | `SnapshotStateList` | 90 | All user profiles (multi-account) | +| [`localUserCreated`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L91) | `MutableState` | 91 | Whether a local user has been created (null = unknown during init) | +| [`setDeliveryReceipts`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L88) | `MutableState` | 88 | Trigger for delivery receipts setup dialog | +| [`switchingUsersAndHosts`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L100) | `MutableState` | 100 | True while switching active user/remote host | +| [`changingActiveUserMutex`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L193) | `Mutex` | 193 | Prevents concurrent user switches | + +### Chat Runtime State + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`chatRunning`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L92) | `MutableState` | 92 | `null` = initializing, `true` = running, `false` = stopped | +| [`chatDbChanged`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L93) | `MutableState` | 93 | Database was changed externally (needs restart) | +| [`chatDbEncrypted`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L94) | `MutableState` | 94 | Whether database is encrypted | +| [`chatDbStatus`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L95) | `MutableState` | 95 | Result of database migration attempt | +| [`ctrlInitInProgress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L96) | `MutableState` | 96 | Controller initialization in progress | +| [`dbMigrationInProgress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L97) | `MutableState` | 97 | Database migration in progress | +| [`incompleteInitializedDbRemoved`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L98) | `MutableState` | 98 | Tracks if incomplete DB files were removed (prevents infinite retry) | + +### Current Chat State + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`chatId`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L103) | `MutableState` | 103 | ID of the currently open chat (null = chat list shown) | +| [`chatAgentConnId`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L104) | `MutableState` | 104 | Agent connection ID for current chat | +| [`chatSubStatus`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L105) | `MutableState` | 105 | Subscription status for current chat | +| [`openAroundItemId`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L106) | `MutableState` | 106 | Item ID to scroll to when opening chat | +| [`chatsContext`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L107) | `ChatsContext` | 107 | Primary chat context (see [ChatsContext](#3-chatscontext)) | +| [`secondaryChatsContext`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L108) | `MutableState` | 108 | Optional secondary context for dual-pane views | +| [`chats`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L110) | `State>` | 110 | Derived from `chatsContext.chats` | +| [`deletedChats`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L112) | `MutableState>>` | 112 | Recently deleted chats (rhId, chatId) | + +### Group Members + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`groupMembers`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L113) | `MutableState>` | 113 | Members of currently viewed group | +| [`groupMembersIndexes`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L114) | `MutableState>` | 114 | Index lookup by `groupMemberId` | +| [`membersLoaded`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L115) | `MutableState` | 115 | Whether group members have been loaded | + +### Chat Tags and Filters + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`userTags`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L118) | `MutableState>` | 118 | User-defined chat tags | +| [`activeChatTagFilter`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L119) | `MutableState` | 119 | Currently active filter in chat list | +| [`presetTags`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L120) | `SnapshotStateMap` | 120 | Counts for preset tag categories (favorites, groups, contacts, etc.) | +| [`unreadTags`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L121) | `SnapshotStateMap` | 121 | Unread counts per user-defined tag | + +### Terminal and Developer + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`terminalsVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L125) | `Set` | 125 | Tracks which terminal views are visible (default vs floating) | +| [`terminalItems`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L126) | `MutableState>` | 126 | Command/response log for developer terminal | + +### Calls (WebRTC) + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`callManager`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L161) | `CallManager` | 161 | WebRTC call lifecycle manager | +| [`callInvitations`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L162) | `SnapshotStateMap` | 162 | Pending incoming call invitations keyed by chatId | +| [`activeCallInvitation`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L163) | `MutableState` | 163 | Currently displayed incoming call invitation | +| [`activeCall`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L164) | `MutableState` | 164 | Currently active call | +| [`activeCallViewIsVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L165) | `MutableState` | 165 | Whether call UI is showing | +| [`activeCallViewIsCollapsed`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L166) | `MutableState` | 166 | Whether call UI is in PiP/collapsed mode | +| [`callCommand`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L167) | `SnapshotStateList` | 167 | Pending WebRTC commands | +| [`showCallView`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L168) | `MutableState` | 168 | Call view visibility toggle | +| [`switchingCall`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L169) | `MutableState` | 169 | True during call switching | + +### Compose Draft and Sharing + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`draft`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L176) | `MutableState` | 176 | Saved compose draft for current chat | +| [`draftChatId`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L177) | `MutableState` | 177 | Chat ID the draft belongs to | +| [`sharedContent`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L180) | `MutableState` | 180 | Content received via share intent or internal forwarding | + +### Remote Hosts + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`remoteHosts`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L199) | `SnapshotStateList` | 199 | Connected remote hosts (for desktop-mobile pairing) | +| [`currentRemoteHost`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L200) | `MutableState` | 200 | Currently selected remote host | +| [`remoteHostPairing`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L203) | `MutableState?>` | 203 | Remote host pairing state | +| [`remoteCtrlSession`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L204) | `MutableState` | 204 | Remote controller session | + +### Miscellaneous UI State + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`userAddress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L127) | `MutableState` | 127 | User's public contact address | +| [`chatItemTTL`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L128) | `MutableState` | 128 | Chat item time-to-live setting | +| [`clearOverlays`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L131) | `MutableState` | 131 | Signal to close all overlays/modals | +| [`appOpenUrl`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L137) | `MutableState?>` | 137 | URL opened via deep link (rhId, uri) | +| [`appOpenUrlConnecting`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L138) | `MutableState` | 138 | Whether a deep link connection is in progress | +| [`newChatSheetVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L141) | `MutableState` | 141 | Whether new chat bottom sheet is visible | +| [`fullscreenGalleryVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L144) | `MutableState` | 144 | Fullscreen gallery mode | +| [`notificationPreviewMode`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L147) | `MutableState` | 147 | Notification content preview level | +| [`showAuthScreen`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L156) | `MutableState` | 156 | Whether to show authentication screen | +| [`showChatPreviews`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L158) | `MutableState` | 158 | Whether to show chat preview text in list | +| [`clipboardHasText`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L185) | `MutableState` | 185 | System clipboard has text content | +| [`networkInfo`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L186) | `MutableState` | 186 | Network type and online status | +| [`conditions`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L188) | `MutableState` | 188 | Server operator terms/conditions | +| [`updatingProgress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L190) | `MutableState` | 190 | Progress indicator for app updates | +| [`simplexLinkMode`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L183) | `MutableState` | 183 | How SimpleX links are displayed | +| [`migrationState`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L174) | `MutableState` | 174 | Database migration to new device state | +| [`showingInvitation`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L172) | `MutableState` | 172 | Currently displayed invitation | +| [`desktopOnboardingRandomPassword`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L134) | `MutableState` | 134 | Desktop: user skipped password setup | +| [`filesToDelete`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L182) | `MutableSet` | 182 | Temporary files pending cleanup | + +--- + + + +## 3. ChatsContext + +Defined as inner class at [`ChatModel.kt line 339`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L339): + +```kotlin +class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) +``` + +`ChatsContext` holds the chat list and current chat items for a given context. The `ChatModel` maintains a **primary** context (`chatsContext` at line 107) and an optional **secondary** context (`secondaryChatsContext` at line 108). + +The secondary context is used for: +- **Group support chat scope** (`SecondaryContextFilter.GroupChatScopeContext`) -- viewing member support threads alongside the main group chat +- **Message content tag filtering** (`SecondaryContextFilter.MsgContentTagContext`) -- filtering messages by content type + +### Fields + +| Field | Type | Line | Purpose | +|---|---|---|---| +| [`secondaryContextFilter`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L339) | `SecondaryContextFilter?` | 339 | Filter type: null = primary, GroupChatScope or MsgContentTag | +| [`chats`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L340) | `MutableState>` | 340 | List of all chats in this context | +| [`chatItems`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L345) | `MutableState>` | 345 | Items for the currently open chat in this context | +| [`chatState`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L347) | `ActiveChatState` | 347 | Tracks unread counts, splits, scroll state | + +### Derived Properties + +| Property | Line | Purpose | +|---|---|---| +| [`contentTag`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L353) | 353 | `MsgContentTag?` -- content filter tag if context is MsgContentTag | +| [`groupScopeInfo`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L360) | 360 | `GroupChatScopeInfo?` -- group scope if context is GroupChatScope | +| [`isUserSupportChat`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L367) | 367 | True when viewing own support chat (no specific member) | + +### Key Operations + +- `addChat(chat)` -- adds chat at index 0, triggers pop animation +- `reorderChat(chat, toIndex)` -- reorders chat list (e.g., when a chat receives a new message) +- `updateChatInfo(rhId, cInfo)` -- updates chat metadata while preserving connection stats +- `hasChat(rhId, id)` / `getChat(id)` -- lookup methods + +### ActiveChatState + +Defined at [`ChatItemsMerger.kt line 196`](../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt#L196): + +```kotlin +data class ActiveChatState( + val splits: MutableStateFlow> = MutableStateFlow(emptyList()), + val unreadAfterItemId: MutableStateFlow = MutableStateFlow(-1L), + val totalAfter: MutableStateFlow = MutableStateFlow(0), + val unreadTotal: MutableStateFlow = MutableStateFlow(0), + val unreadAfter: MutableStateFlow = MutableStateFlow(0), + val unreadAfterNewestLoaded: MutableStateFlow = MutableStateFlow(0) +) +``` + +This tracks the scroll position and unread item accounting for the lazy-loaded chat item list: + +| Field | Purpose | +|---|---| +| `splits` | List of item IDs where pagination gaps exist (items not yet loaded) | +| `unreadAfterItemId` | The item ID that marks the boundary of "read" vs "unread after" | +| `totalAfter` | Total items after the unread boundary | +| `unreadTotal` | Total unread items in the chat | +| `unreadAfter` | Unread items after the boundary (exclusive) | +| `unreadAfterNewestLoaded` | Unread items after the newest loaded batch | + +--- + + + +## 4. Chat + +Defined at [`ChatModel.kt line 1328`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L1328): + +```kotlin +@Serializable @Stable +data class Chat( + val remoteHostId: Long?, + val chatInfo: ChatInfo, + val chatItems: List, + val chatStats: ChatStats = ChatStats() +) +``` + +### Fields + +| Field | Type | Purpose | +|---|---|---| +| `remoteHostId` | `Long?` | Remote host ID (null = local) | +| `chatInfo` | `ChatInfo` | Sealed class: `Direct`, `Group`, `Local`, `ContactRequest`, `ContactConnection`, `InvalidJSON` | +| `chatItems` | `List` | Latest chat items (summary; full list is in `ChatsContext.chatItems`) | +| `chatStats` | `ChatStats` | Unread counts and stats | + + + +### ChatStats + +Defined at [`ChatModel.kt line 1370`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L1370): + +```kotlin +data class ChatStats( + val unreadCount: Int = 0, + val unreadMentions: Int = 0, + val reportsCount: Int = 0, + val minUnreadItemId: Long = 0, + val unreadChat: Boolean = false +) +``` + +### Derived Properties + +| Property | Line | Purpose | +|---|---|---| +| `id` | 1349 | Chat ID derived from `chatInfo.id` | +| `unreadTag` | 1343 | Whether chat counts as "unread" for tag filtering (considers notification settings) | +| `supportUnreadCount` | 1351 | Unread count in support/moderation context | +| `nextSendGrpInv` | 1337 | Whether next message should send group invitation | + + + +### ChatInfo Variants + +`ChatInfo` is a sealed class at [`ChatModel.kt line 1391`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L1391): + +| Variant | SerialName | Key Data | +|---|---|---| +| `ChatInfo.Direct` | `"direct"` | `contact: Contact` | +| `ChatInfo.Group` | `"group"` | `groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?` | +| `ChatInfo.Local` | `"local"` | `noteFolder: NoteFolder` | +| `ChatInfo.ContactRequest` | `"contactRequest"` | `contactRequest: UserContactRequest` | +| `ChatInfo.ContactConnection` | `"contactConnection"` | `contactConnection: PendingContactConnection` | +| `ChatInfo.InvalidJSON` | `"invalidJSON"` | `json: String` | + +--- + + + + +## 5. AppPreferences + +Defined at [`SimpleXAPI.kt line 94`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L94) as `class AppPreferences`. + +Uses the `multiplatform-settings` library (`com.russhwolf.settings.Settings`) for cross-platform key-value storage (Android `SharedPreferences` / Desktop `java.util.prefs.Preferences`). + +The `AppPreferences` instance is created lazily in `ChatController` at [`SimpleXAPI.kt line 496`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L496): +```kotlin +val appPrefs: AppPreferences by lazy { AppPreferences() } +``` + +### Preference Categories + +#### Notifications (lines 96-103) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `notificationsMode` | `NotificationsMode` | `SERVICE` (if previously enabled) | OFF / SERVICE / PERIODIC | +| `notificationPreviewMode` | `String` | `"message"` | message / contact / hidden | +| `canAskToEnableNotifications` | `Boolean` | `true` | Whether to show notification enable prompt | +| `backgroundServiceNoticeShown` | `Boolean` | `false` | Background service notice already shown | +| `backgroundServiceBatteryNoticeShown` | `Boolean` | `false` | Battery notice already shown | +| `autoRestartWorkerVersion` | `Int` | `0` | Worker version for periodic restart | + +#### Calls (lines 105-111) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `webrtcPolicyRelay` | `Boolean` | `true` | Use TURN relay for WebRTC | +| `callOnLockScreen` | `CallOnLockScreen` | `SHOW` | DISABLE / SHOW / ACCEPT | +| `webrtcIceServers` | `String?` | `null` | Custom ICE servers | +| `experimentalCalls` | `Boolean` | `false` | Enable experimental call features | + +#### Authentication (lines 107-110) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `performLA` | `Boolean` | `false` | Enable local authentication | +| `laMode` | `LAMode` | default | Authentication mode | +| `laLockDelay` | `Int` | `30` | Seconds before re-auth required | +| `laNoticeShown` | `Boolean` | `false` | LA notice shown | + +#### Privacy (lines 112-128) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `privacyProtectScreen` | `Boolean` | `true` | FLAG_SECURE on Android | +| `privacyAcceptImages` | `Boolean` | `true` | Auto-accept images | +| `privacyLinkPreviews` | `Boolean` | `true` | Generate link previews | +| `privacySanitizeLinks` | `Boolean` | `false` | Remove tracking params from links | +| `simplexLinkMode` | `SimplexLinkMode` | `DESCRIPTION` | DESCRIPTION / FULL / BROWSER | +| `privacyShowChatPreviews` | `Boolean` | `true` | Show chat previews in list | +| `privacySaveLastDraft` | `Boolean` | `true` | Save compose draft | +| `privacyDeliveryReceiptsSet` | `Boolean` | `false` | Delivery receipts configured | +| `privacyEncryptLocalFiles` | `Boolean` | `true` | Encrypt local files | +| `privacyAskToApproveRelays` | `Boolean` | `true` | Ask before using relays | +| `privacyMediaBlurRadius` | `Int` | `0` | Blur radius for media | + +#### Network (lines 140-175) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `networkUseSocksProxy` | `Boolean` | `false` | Enable SOCKS proxy | +| `networkProxy` | `NetworkProxy` | localhost:9050 | Proxy host/port | +| `networkSessionMode` | `TransportSessionMode` | default | Session mode | +| `networkSMPProxyMode` | `SMPProxyMode` | default | SMP proxy mode | +| `networkSMPProxyFallback` | `SMPProxyFallback` | default | Proxy fallback policy | +| `networkHostMode` | `HostMode` | default | Host mode (onion routing) | +| `networkRequiredHostMode` | `Boolean` | `false` | Enforce host mode | +| `networkSMPWebPortServers` | `SMPWebPortServers` | default | Web port server config | +| `networkShowSubscriptionPercentage` | `Boolean` | `false` | Show subscription stats | +| `networkTCPConnectTimeout*` | `Long` | varies | TCP connect timeouts (background/interactive) | +| `networkTCPTimeout*` | `Long` | varies | TCP operation timeouts | +| `networkTCPTimeoutPerKb` | `Long` | varies | Per-KB timeout | +| `networkRcvConcurrency` | `Int` | default | Receive concurrency | +| `networkSMPPingInterval` | `Long` | default | SMP ping interval | +| `networkSMPPingCount` | `Int` | default | SMP ping count | +| `networkEnableKeepAlive` | `Boolean` | default | TCP keep-alive | +| `networkTCPKeepIdle` | `Int` | default | Keep-alive idle time | +| `networkTCPKeepIntvl` | `Int` | default | Keep-alive interval | +| `networkTCPKeepCnt` | `Int` | default | Keep-alive count | + +#### Appearance (lines 213-233) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `currentTheme` | `String` | `"SYSTEM"` | Active theme name | +| `systemDarkTheme` | `String` | `"SIMPLEX"` | Theme for system dark mode | +| `currentThemeIds` | `Map` | empty | Theme ID per base theme | +| `themeOverrides` | `List` | empty | Custom theme overrides | +| `profileImageCornerRadius` | `Float` | `22.5f` | Avatar corner radius | +| `chatItemRoundness` | `Float` | `0.75f` | Message bubble roundness | +| `chatItemTail` | `Boolean` | `true` | Show bubble tail | +| `fontScale` | `Float` | `1f` | Font scale factor | +| `densityScale` | `Float` | `1f` | UI density scale | +| `inAppBarsAlpha` | `Float` | varies | Bar transparency | +| `appearanceBarsBlurRadius` | `Int` | 50 or 0 | Bar blur radius (device-dependent) | + +#### Developer (lines 135-139) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `developerTools` | `Boolean` | `false` | Enable developer tools | +| `logLevel` | `LogLevel` | `WARNING` | Log level | +| `showInternalErrors` | `Boolean` | `false` | Show internal errors to user | +| `showSlowApiCalls` | `Boolean` | `false` | Alert on slow API calls | +| `terminalAlwaysVisible` | `Boolean` | `false` | Floating terminal window (desktop) | + +#### Database (lines 188-208) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `onboardingStage` | `OnboardingStage` | `OnboardingComplete` | Current onboarding step | +| `storeDBPassphrase` | `Boolean` | `true` | Store DB passphrase in keystore | +| `initialRandomDBPassphrase` | `Boolean` | `false` | DB was created with random passphrase | +| `encryptedDBPassphrase` | `String?` | null | Encrypted DB passphrase | +| `confirmDBUpgrades` | `Boolean` | `false` | Confirm DB migrations | +| `chatStopped` | `Boolean` | `false` | Chat was explicitly stopped | +| `chatLastStart` | `Instant?` | null | Last chat start timestamp | +| `newDatabaseInitialized` | `Boolean` | `false` | DB successfully initialized at least once | +| `shouldImportAppSettings` | `Boolean` | `false` | Import settings after DB import | +| `selfDestruct` | `Boolean` | `false` | Self-destruct enabled | +| `selfDestructDisplayName` | `String?` | null | Display name for self-destruct profile | + +#### UI Preferences (lines 255-257) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `oneHandUI` | `Boolean` | `true` | One-hand mode | +| `chatBottomBar` | `Boolean` | `true` | Bottom bar in chat | + +#### Remote Access (lines 238-243) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `deviceNameForRemoteAccess` | `String` | device model | Device name shown to paired devices | +| `confirmRemoteSessions` | `Boolean` | `false` | Confirm remote sessions | +| `connectRemoteViaMulticast` | `Boolean` | `false` | Use multicast for discovery | +| `connectRemoteViaMulticastAuto` | `Boolean` | `true` | Auto-connect via multicast | +| `offerRemoteMulticast` | `Boolean` | `true` | Offer multicast connection | + +#### Migration (lines 189-190) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `migrationToStage` | `String?` | null | Migration-to-device progress | +| `migrationFromStage` | `String?` | null | Migration-from-device progress | + +#### Updates and Versioning (lines 184-186, 235-237) + +| Key | Type | Default | Purpose | +|---|---|---|---| +| `appUpdateChannel` | `AppUpdatesChannel` | `DISABLED` | DISABLED / STABLE / BETA | +| `appSkippedUpdate` | `String` | `""` | Skipped update version | +| `appUpdateNoticeShown` | `Boolean` | `false` | Update notice shown | +| `whatsNewVersion` | `String?` | null | Last "What's New" version seen | +| `lastMigratedVersionCode` | `Int` | `0` | Last app version code for data migrations | +| `customDisappearingMessageTime` | `Int` | `300` | Custom disappearing message time (seconds) | + +### Preference Utility Types + +The `SharedPreference` wrapper (defined in SimpleXAPI.kt) provides: +- `get(): T` -- read current value +- `set(value: T)` -- write value +- `state: MutableState` -- Compose-observable state (derived lazily) + +Factory methods: `mkBoolPreference`, `mkIntPreference`, `mkLongPreference`, `mkFloatPreference`, `mkStrPreference`, `mkEnumPreference`, `mkSafeEnumPreference`, `mkDatePreference`, `mkMapPreference`, `mkTimeoutPreference`. + +--- + +## 6. Source Files + +| File | Path | Key Contents | +|---|---|---| +| ChatModel.kt | [`common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt) | `ChatModel` singleton (line 86), `ChatsContext` (line 339), `Chat` (line 1328), `ChatInfo` (line 1391), `ChatStats` (line 1370), helper methods | +| SimpleXAPI.kt | [`common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt) | `AppPreferences` (line 94), `ChatController` (line 493), `startReceiver` (line 660), `sendCmd` (line 804), `recvMsg` (line 829), `processReceivedMsg` (line 2568) | +| ChatItemsMerger.kt | [`common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt`](../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt) | `ActiveChatState` (line 196), chat item merge/diff logic | +| Core.kt | [`common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt) | `initChatController` (line 62), state initialization flow | +| App.kt | [`common/src/commonMain/kotlin/chat/simplex/common/App.kt`](../common/src/commonMain/kotlin/chat/simplex/common/App.kt) | `AppScreen` (line 47), `MainScreen` (line 84), top-level UI state reads | diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 809c67101d..268e4329cc 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -94,6 +94,5 @@ mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, - createBot = Just CreateBotOpts {botDisplayName, allowFiles = False, clientService = False}, - maintenance = False + createBot = Just CreateBotOpts {botDisplayName, allowFiles = False, clientService = False} } diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 1f075c677c..45c0b84cc6 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -10,11 +10,13 @@ module Directory.Events ( DirectoryEvent (..), DirectoryCmd (..), + DirectoryCmdTag (..), ADirectoryCmd (..), DirectoryHelpSection (..), DirectoryRole (..), SDirectoryRole (..), crDirectoryEvent, + directoryCmdP, directoryCmdTag, ) where diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index e093678b73..a44e55e376 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -9,6 +9,7 @@ module Directory.Options ( DirectoryOpts (..), MigrateLog (..), getDirectoryOpts, + directoryOpts, mkChatOpts, ) where @@ -27,12 +28,14 @@ data DirectoryOpts = DirectoryOpts adminUsers :: [KnownContact], superUsers :: [KnownContact], ownersGroup :: Maybe KnownGroup, + noAddress :: Bool, -- skip creating address blockedWordsFile :: Maybe FilePath, blockedFragmentsFile :: Maybe FilePath, blockedExtensionRules :: Maybe FilePath, nameSpellingFile :: Maybe FilePath, profileNameLimit :: Int, captchaGenerator :: Maybe FilePath, + voiceCaptchaGenerator :: Maybe FilePath, directoryLog :: Maybe FilePath, migrateDirectoryLog :: Maybe MigrateLog, serviceName :: T.Text, @@ -71,6 +74,11 @@ directoryOpts appDir defaultDbName = do <> metavar "OWNERS_GROUP" <> help "The group of group owners in the format GROUP_ID:DISPLAY_NAME - owners of listed groups will be invited automatically" ) + noAddress <- + switch + ( long "no-address" + <> help "skip checking and creating service address" + ) blockedWordsFile <- optional $ strOption @@ -114,6 +122,13 @@ directoryOpts appDir defaultDbName = do <> metavar "CAPTCHA_GENERATOR" <> help "Executable to generate captcha files, must accept text as parameter and save file to stdout as base64 up to 12500 bytes" ) + voiceCaptchaGenerator <- + optional $ + strOption + ( long "voice-captcha-generator" + <> metavar "VOICE_CAPTCHA_GENERATOR" + <> help "Executable to generate voice captcha, accepts text as parameter, writes audio file, outputs file_path and duration_seconds to stdout" + ) directoryLog <- optional $ strOption @@ -159,12 +174,14 @@ directoryOpts appDir defaultDbName = do adminUsers, superUsers, ownersGroup, + noAddress, blockedWordsFile, blockedFragmentsFile, blockedExtensionRules, nameSpellingFile, profileNameLimit, captchaGenerator, + voiceCaptchaGenerator, directoryLog, migrateDirectoryLog, serviceName = T.pack serviceName, @@ -201,8 +218,7 @@ mkChatOpts DirectoryOpts {coreOptions, serviceName, clientService} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, - createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False, clientService}, - maintenance = False + createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False, clientService} } parseMigrateLog :: ReadM MigrateLog diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 5901e1d61a..3b8391ada0 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -20,11 +20,14 @@ where import Control.Concurrent (forkIO) import Control.Concurrent.STM +import Control.Exception (SomeException, try) import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import qualified Data.Attoparsec.Text as A import Data.Bifunctor (first) +import Data.Either (fromRight) import Data.List (find, intercalate) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.Map.Strict as M @@ -51,7 +54,7 @@ import Simplex.Chat.Core import Simplex.Chat.Markdown (Format (..), FormattedText (..), parseMaybeMarkdownList, viewName) import Simplex.Chat.Messages import Simplex.Chat.Options -import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Protocol (MsgContent (..), memberSupportVoiceVersion) import Simplex.Chat.Store.Direct (getContact) import Simplex.Chat.Store.Groups (getGroupLink, getGroupMember, setGroupCustomData) -- TODO remove setGroupCustomData import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo) @@ -63,13 +66,15 @@ import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact) +import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (eitherToMaybe, raceAny_, safeDecodeUtf8, tshow, unlessM, (<$$>)) -import System.Directory (getAppUserDataDirectory) +import System.Directory (getAppUserDataDirectory, removeFile) import System.Exit (exitFailure) import System.Process (readProcess) +import Text.Read (readMaybe) data GroupProfileUpdate = GPNoServiceLink @@ -97,10 +102,13 @@ data ServiceState = ServiceState updateListingsJob :: TMVar ChatController } +data CaptchaMode = CMText | CMAudio + data PendingCaptcha = PendingCaptcha { captchaText :: Text, sentAt :: UTCTime, - attempts :: Int + attempts :: Int, + captchaMode :: CaptchaMode } captchaLength :: Int @@ -184,10 +192,11 @@ directoryPreStartHook :: DirectoryOpts -> ChatController -> IO () directoryPreStartHook opts ChatController {config, chatStore} = runDirectoryMigrations opts config chatStore directoryPostStartHook :: DirectoryOpts -> ServiceState -> ChatController -> IO () -directoryPostStartHook opts env cc = +directoryPostStartHook opts@DirectoryOpts {noAddress, testing} env cc = readTVarIO (currentUser cc) >>= \case Nothing -> putStrLn "No current user" >> exitFailure Just User {userId, profile = p@LocalProfile {preferences}} -> do + unless noAddress $ initializeBotAddress' (not testing) cc listingsUpdated env cc let cmds = fromMaybe [] $ preferences >>= commands_ unless (cmds == directoryCommands) $ do @@ -216,7 +225,7 @@ directoryCommands = idParam = Just "" directoryService :: DirectoryLog -> DirectoryOpts -> ChatConfig -> IO () -directoryService st opts@DirectoryOpts {testing} cfg = do +directoryService st opts cfg = do env <- newServiceState opts let chatHooks = defaultChatHooks @@ -224,8 +233,7 @@ directoryService st opts@DirectoryOpts {testing} cfg = do postStartHook = Just $ directoryPostStartHook opts env, acceptMember = Just $ acceptMemberHook opts env } - simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \user cc -> do - initializeBotAddress' (not testing) cc + simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \user cc -> raceAny_ $ [ forever $ void getLine, forever $ do @@ -555,33 +563,62 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName dePendingMember :: GroupInfo -> GroupMember -> IO () dePendingMember g@GroupInfo {groupProfile = GroupProfile {displayName}} m - | memberRequiresCaptcha a m = sendMemberCaptcha g m Nothing captchaNotice 0 + | memberRequiresCaptcha a m = sendMemberCaptcha g m Nothing captchaNotice 0 CMText | otherwise = approvePendingMember a g m where a = groupMemberAcceptance g - captchaNotice = "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." + captchaNotice = + "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." + <> if canSendVoiceCaptcha g m then "\nSend /audio to receive a voice captcha." else "" - sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> IO () - sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts = do + sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> CaptchaMode -> IO () + sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts mode = do s <- getCaptchaStr captchaLength "" - mc <- getCaptcha s sentAt <- getCurrentTime - let captcha = PendingCaptcha {captchaText = T.pack s, sentAt, attempts = prevAttempts + 1} + let captcha = PendingCaptcha {captchaText = T.pack s, sentAt, attempts = prevAttempts + 1, captchaMode = mode} atomically $ TM.insert gmId captcha $ pendingCaptchas env - sendCaptcha mc + case mode of + CMAudio -> do + mc <- getCaptchaContent s + sendComposedMessages_ cc sendRef [(quotedId, MCText noticeText), (Nothing, mc)] + sendVoiceCaptcha sendRef s + CMText -> do + mc <- getCaptchaContent s + sendComposedMessages_ cc sendRef [(quotedId, MCText noticeText), (Nothing, mc)] where - getCaptcha s = case captchaGenerator opts of - Nothing -> pure textMsg - Just script -> content <$> readProcess script [s] "" - where - textMsg = MCText $ T.pack s - content r = case T.lines $ T.pack r of - [] -> textMsg - "" : _ -> textMsg - img : _ -> MCImage "" $ ImageData img - sendCaptcha mc = sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [(quotedId, MCText noticeText), (Nothing, mc)] + sendRef = SRGroup groupId $ Just $ GCSMemberSupport (Just gmId) gmId = groupMemberId' m + sendVoiceCaptcha :: SendRef -> String -> IO () + sendVoiceCaptcha sendRef s = + forM_ (voiceCaptchaGenerator opts) $ \script -> + void . forkIO $ do + voiceResult <- try $ readProcess script [s] "" :: IO (Either SomeException String) + case voiceResult of + Right r -> case lines r of + (filePath : durationStr : _) + | not (null filePath), Just duration <- readMaybe durationStr -> do + sendComposedMessageFile cc sendRef Nothing (MCVoice "" duration) (CF.plain filePath) + void (try $ removeFile filePath :: IO (Either SomeException ())) + _ -> logError "voice captcha generator: unexpected output" + Left e -> logError $ "voice captcha generator error: " <> tshow e + + getCaptchaContent :: String -> IO MsgContent + getCaptchaContent s = case captchaGenerator opts of + Nothing -> pure $ MCText $ T.pack s + Just script -> content <$> readProcess script [s] "" + where + content r = case T.lines $ T.pack r of + [] -> textMsg + "" : _ -> textMsg + img : _ -> MCImage "" $ ImageData img + textMsg = MCText $ T.pack s + + canSendVoiceCaptcha :: GroupInfo -> GroupMember -> Bool + canSendVoiceCaptcha gInfo m = + isJust (voiceCaptchaGenerator opts) + && (groupFeatureUserAllowed SGFVoice gInfo || supportsVersion m memberSupportVoiceVersion) + approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO () approvePendingMember a g@GroupInfo {groupId} m@GroupMember {memberProfile = LocalProfile {displayName, image}} = do gli_ <- join . eitherToMaybe <$> withDB' "getGroupLinkInfo" cc (\db -> getGroupLinkInfo db userId groupId) @@ -598,16 +635,38 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName dePendingMemberMsg :: GroupInfo -> GroupMember -> ChatItemId -> Text -> IO () dePendingMemberMsg g@GroupInfo {groupId, groupProfile = GroupProfile {displayName = n}} m@GroupMember {memberProfile = LocalProfile {displayName}} ciId msgText | memberRequiresCaptcha a m = do - ts <- getCurrentTime - atomically (TM.lookup (groupMemberId' m) $ pendingCaptchas env) >>= \case - Just PendingCaptcha {captchaText, sentAt, attempts} - | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired $ attempts - 1 - | matchCaptchaStr captchaText msgText -> do - sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just $ groupMemberId' m)) [(Just ciId, MCText $ "Correct, you joined the group " <> n)] - approvePendingMember a g m - | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts - | otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts - Nothing -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 + let gmId = groupMemberId' m + sendRef = SRGroup groupId $ Just $ GCSMemberSupport (Just gmId) + -- /audio is matched as text, not as DirectoryCmd, because it is only valid + -- in group context at captcha stage, while DirectoryCmd is for DM commands. + isAudioCmd = T.strip msgText == "/audio" + cmd = fromRight (ADC SDRUser DCUnknownCommand) $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.strip msgText + atomically (TM.lookup gmId $ pendingCaptchas env) >>= \case + Nothing + | isAudioCmd && canSendVoiceCaptcha g m -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 CMAudio + | isAudioCmd -> sendComposedMessages_ cc sendRef [(Just ciId, MCText voiceCaptchaUnavailable)] + | otherwise -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 CMText + Just pc@PendingCaptcha {captchaText, sentAt, attempts, captchaMode} + | isAudioCmd -> + if canSendVoiceCaptcha g m + then case captchaMode of + CMText -> do + atomically $ TM.insert gmId pc {captchaMode = CMAudio} $ pendingCaptchas env + sendVoiceCaptcha sendRef (T.unpack captchaText) + CMAudio -> + sendComposedMessages_ cc sendRef [(Just ciId, MCText audioAlreadyEnabled)] + else sendComposedMessages_ cc sendRef [(Just ciId, MCText voiceCaptchaUnavailable)] + | otherwise -> case cmd of + ADC SDRUser (DCSearchGroup _) -> do + ts <- getCurrentTime + if + | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired (attempts - 1) captchaMode + | matchCaptchaStr captchaText msgText -> do + sendComposedMessages_ cc sendRef [(Just ciId, MCText $ "Correct, you joined the group " <> n)] + approvePendingMember a g m + | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts + | otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts captchaMode + _ -> sendComposedMessages_ cc sendRef [(Just ciId, MCText unknownCommand)] | otherwise = approvePendingMember a g m where a = groupMemberAcceptance g @@ -619,11 +678,21 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName atomically $ TM.delete gmId $ pendingCaptchas env logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g r -> logError $ "unexpected remove member response: " <> tshow r + captchaExpired :: Text captchaExpired = "Captcha expired, please try again." + wrongCaptcha :: Int -> Text wrongCaptcha attempts | attempts == maxCaptchaAttempts - 1 = "Incorrect text, please try again - this is your last attempt." | otherwise = "Incorrect text, please try again." + noCaptcha :: Text noCaptcha = "Unexpected message, please try again." + audioAlreadyEnabled :: Text + audioAlreadyEnabled = "Audio captcha is already enabled." + voiceCaptchaUnavailable :: Text + voiceCaptchaUnavailable = "Voice captcha is not available - please update SimpleX Chat to v6.5+ or use text captcha." + unknownCommand :: Text + unknownCommand = "Unknown command, please enter captcha text." + tooManyAttempts :: Text tooManyAttempts = "Too many failed attempts, you can't join group." memberRequiresCaptcha :: DirectoryMemberAcceptance -> GroupMember -> Bool @@ -639,8 +708,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName <> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:") msg = maybe (MCText text) (\image -> MCImage {text, image}) image' withAdminUsers $ \cId -> do - sendComposedMessage' cc cId Nothing msg - sendMessage' cc cId $ "/approve " <> tshow groupId <> ":" <> viewName displayName <> " " <> tshow gaId <> if promoted then " promote=on" else "" + let approveCmd = MCText $ "/approve " <> tshow groupId <> ":" <> viewName displayName <> " " <> tshow gaId <> if promoted then " promote=on" else "" + sendComposedMessages cc (SRDirect cId) [msg, approveCmd] deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () deContactRoleChanged g@GroupInfo {groupId, membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do @@ -665,7 +734,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName where rStatus = groupRolesStatus contactRole serviceRole groupRef = groupReference g - ctRole = "*" <> strEncodeTxt contactRole <> "*" + ctRole = "*" <> textEncode contactRole <> "*" suCtRole = "(user role is set to " <> ctRole <> ")." deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () @@ -691,7 +760,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName _ -> pure () where groupRef = groupReference g - srvRole = "*" <> strEncodeTxt serviceRole <> "*" + srvRole = "*" <> textEncode serviceRole <> "*" suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." whenContactIsOwner gr action = getOwnerGroupMember groupId gr @@ -801,7 +870,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let anotherRole = case acceptMemberRole of GRObserver -> GRMember; _ -> GRObserver sendReply $ initialRole n acceptMemberRole - <> ("Send /'role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "' to change it.\n\n") + <> ("Send /'role " <> tshow gId <> " " <> textEncode anotherRole <> "' to change it.\n\n") <> onlyViaLink gLink Left _ -> sendReply $ "Error: failed reading the initial member role for the group " <> n Just mRole -> do @@ -809,7 +878,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just gLink -> sendReply $ initialRole n mRole <> "\n" <> onlyViaLink gLink Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated." where - initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n" + initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> textEncode mRole <> "*\n" onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> groupLinkText gLink DCGroupFilter gId gName_ acceptance_ -> (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do @@ -852,7 +921,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName T.unlines $ [ "The link to join the group " <> groupRef <> ":", groupLinkText gLink, - "New member role: " <> strEncodeTxt acceptMemberRole + "New member role: " <> textEncode acceptMemberRole ] <> ["The link is being upgraded..." | shouldBeUpgraded] when shouldBeUpgraded $ do diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index b78b446821..b5f7220724 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -351,11 +351,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId) pure (gs, n) where countQuery' = countQuery <> " WHERE r.group_reg_status = ? " - orderBy = " ORDER BY g.summary_current_members_count DESC " + orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC " STRecent -> case lastGroup_ of Nothing -> do gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize) @@ -363,11 +363,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId) pure (gs, n) where countQuery' = countQuery <> " WHERE r.group_reg_status = ? " - orderBy = " ORDER BY r.created_at DESC " + orderBy = " ORDER BY r.created_at DESC, r.group_reg_id ASC " STSearch search -> case lastGroup_ of Nothing -> do gs <- groups $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize) @@ -375,12 +375,12 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond <> orderBy) (GRSActive, gId, s, s, s, s) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s) pure (gs, n) where s = T.toLower search countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? " - orderBy = " ORDER BY g.summary_current_members_count DESC " + orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC " where groups = (map (toGroupInfoReg (vr cc) user) <$>) count = maybeFirstRow' 0 fromOnly diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index f3bb915665..9f63770df6 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -50,6 +50,9 @@ This file is generated automatically. - [APIListContacts](#apilistcontacts) - [APIListGroups](#apilistgroups) - [APIDeleteChat](#apideletechat) +- [APISetGroupCustomData](#apisetgroupcustomdata) +- [APISetContactCustomData](#apisetcontactcustomdata) +- [APISetUserAutoAcceptMemberContacts](#apisetuserautoacceptmembercontacts) [User profile commands](#user-profile-commands) - [ShowActiveUser](#showactiveuser) @@ -60,6 +63,10 @@ This file is generated automatically. - [APIUpdateProfile](#apiupdateprofile) - [APISetContactPrefs](#apisetcontactprefs) +[Chat management](#chat-management) +- [StartChat](#startchat) +- [APIStopChat](#apistopchat) + --- @@ -98,7 +105,7 @@ UserContactLinkCreated: User contact address created. - user: [User](./TYPES.md#user) - connLinkContact: [CreatedConnLink](./TYPES.md#createdconnlink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -134,7 +141,7 @@ UserContactLinkDeleted: User contact address deleted. - type: "userContactLinkDeleted" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -171,7 +178,7 @@ UserContactLink: User contact address. - user: [User](./TYPES.md#user) - contactLink: [UserContactLink](./TYPES.md#usercontactlink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -211,7 +218,7 @@ UserProfileUpdated: User profile updated. - toProfile: [Profile](./TYPES.md#profile) - updateSummary: [UserProfileUpdateSummary](./TYPES.md#userprofileupdatesummary) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -249,7 +256,7 @@ UserContactLinkUpdated: User contact address updated. - user: [User](./TYPES.md#user) - contactLink: [UserContactLink](./TYPES.md#usercontactlink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -280,7 +287,7 @@ Send messages. ``` ```javascript -'/_send ' + sendRef.toString() + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript +'/_send ' + ChatRef.cmdString(sendRef) + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript ``` ```python @@ -294,7 +301,7 @@ NewChatItems: New messages. - user: [User](./TYPES.md#user) - chatItems: [[AChatItem](./TYPES.md#achatitem)] -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -320,7 +327,7 @@ Update message. ``` ```javascript -'/_update item ' + chatRef.toString() + ' ' + chatItemId + (liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(updatedMessage) // JavaScript +'/_update item ' + ChatRef.cmdString(chatRef) + ' ' + chatItemId + (liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(updatedMessage) // JavaScript ``` ```python @@ -339,7 +346,7 @@ ChatItemNotChanged: Message not changed. - user: [User](./TYPES.md#user) - chatItem: [AChatItem](./TYPES.md#achatitem) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -367,7 +374,7 @@ Delete message. ``` ```javascript -'/_delete item ' + chatRef.toString() + ' ' + chatItemIds.join(',') + ' ' + deleteMode // JavaScript +'/_delete item ' + ChatRef.cmdString(chatRef) + ' ' + chatItemIds.join(',') + ' ' + deleteMode // JavaScript ``` ```python @@ -383,7 +390,7 @@ ChatItemsDeleted: Messages deleted. - byUser: bool - timed: bool -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -423,7 +430,7 @@ ChatItemsDeleted: Messages deleted. - byUser: bool - timed: bool -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -449,7 +456,7 @@ Add/remove message reaction. ``` ```javascript -'/_reaction ' + chatRef.toString() + ' ' + chatItemId + ' ' + (add ? 'on' : 'off') + ' ' + JSON.stringify(reaction) // JavaScript +'/_reaction ' + ChatRef.cmdString(chatRef) + ' ' + chatItemId + ' ' + (add ? 'on' : 'off') + ' ' + JSON.stringify(reaction) // JavaScript ``` ```python @@ -464,7 +471,7 @@ ChatItemReaction: Message reaction. - added: bool - reaction: [ACIReaction](./TYPES.md#acireaction) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -515,7 +522,7 @@ RcvFileAcceptedSndCancelled: File accepted, but no longer sent. - user: [User](./TYPES.md#user) - rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -560,7 +567,7 @@ RcvFileCancelled: Cancelled receiving file. - chatItem_: [AChatItem](./TYPES.md#achatitem)? - rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -609,7 +616,7 @@ SentGroupInvitation: Group invitation sent. - contact: [Contact](./TYPES.md#contact) - member: [GroupMember](./TYPES.md#groupmember) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -647,7 +654,7 @@ UserAcceptedGroupSent: User accepted group invitation. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - hostContact: [Contact](./TYPES.md#contact)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -687,7 +694,7 @@ MemberAccepted: Member accepted to group. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - member: [GroupMember](./TYPES.md#groupmember) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -731,7 +738,7 @@ MembersRoleUser: Members role changed by user. - members: [[GroupMember](./TYPES.md#groupmember)] - toRole: [GroupMemberRole](./TYPES.md#groupmemberrole) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -772,7 +779,7 @@ MembersBlockedForAllUser: Members blocked for all by admin. - members: [[GroupMember](./TYPES.md#groupmember)] - blocked: bool -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -813,7 +820,7 @@ UserDeletedMembers: Members deleted. - members: [[GroupMember](./TYPES.md#groupmember)] - withMessages: bool -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -853,7 +860,7 @@ LeftMemberUser: User left group. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -890,7 +897,7 @@ GroupMembers: Group members. - user: [User](./TYPES.md#user) - group: [Group](./TYPES.md#group) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -929,7 +936,7 @@ GroupCreated: Group created. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -969,7 +976,7 @@ GroupUpdated: Group updated. - toGroup: [GroupInfo](./TYPES.md#groupinfo) - member_: [GroupMember](./TYPES.md#groupmember)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1013,7 +1020,7 @@ GroupLinkCreated: Group link created. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1052,7 +1059,7 @@ GroupLink: Group link. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1089,7 +1096,7 @@ GroupLinkDeleted: Group link deleted. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1127,7 +1134,7 @@ GroupLink: Group link. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1171,7 +1178,7 @@ Invitation: One-time invitation. - connLinkInvitation: [CreatedConnLink](./TYPES.md#createdconnlink) - connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1210,7 +1217,7 @@ ConnectionPlan: Connection link information. - connLink: [CreatedConnLink](./TYPES.md#createdconnlink) - connectionPlan: [ConnectionPlan](./TYPES.md#connectionplan) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1219,7 +1226,7 @@ ChatCmdError: Command error. ### APIConnect -Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link +Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link. *Network usage*: interactive. @@ -1235,7 +1242,7 @@ Connect via prepared SimpleX link. The link can be 1-time invitation link, conta ``` ```javascript -'/_connect ' + userId + (preparedLink_ ? ' ' + preparedLink_.toString() : '') // JavaScript +'/_connect ' + userId + (preparedLink_ ? ' ' + CreatedConnLink.cmdString(preparedLink_) : '') // JavaScript ``` ```python @@ -1261,7 +1268,7 @@ SentInvitation: Invitation sent to contact address. - connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) - customUserProfile: [Profile](./TYPES.md#profile)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1311,7 +1318,7 @@ SentInvitation: Invitation sent to contact address. - connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) - customUserProfile: [Profile](./TYPES.md#profile)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1348,7 +1355,7 @@ AcceptingContactRequest: Contact request accepted. - user: [User](./TYPES.md#user) - contact: [Contact](./TYPES.md#contact) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1386,7 +1393,7 @@ ContactRequestRejected: Contact request rejected. - contactRequest: [UserContactRequest](./TYPES.md#usercontactrequest) - contact_: [Contact](./TYPES.md#contact)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1428,7 +1435,7 @@ ContactsList: Contacts. - user: [User](./TYPES.md#user) - contacts: [[Contact](./TYPES.md#contact)] -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1467,7 +1474,7 @@ GroupsList: Groups. - user: [User](./TYPES.md#user) - groups: [[GroupInfo](./TYPES.md#groupinfo)] -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1491,7 +1498,7 @@ Delete chat. ``` ```javascript -'/_delete ' + chatRef.toString() + ' ' + chatDeleteMode.toString() // JavaScript +'/_delete ' + ChatRef.cmdString(chatRef) + ' ' + ChatDeleteMode.cmdString(chatDeleteMode) // JavaScript ``` ```python @@ -1515,7 +1522,118 @@ GroupDeletedUser: User deleted group. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + +### APISetGroupCustomData + +Set group custom data. + +*Network usage*: no. + +**Parameters**: +- groupId: int64 +- customData: JSONObject? + +**Syntax**: + +``` +/_set custom #[ ] +``` + +```javascript +'/_set custom #' + groupId + (customData ? ' ' + JSON.stringify(customData) : '') // JavaScript +``` + +```python +'/_set custom #' + str(groupId) + ((' ' + json.dumps(customData)) if customData is not None else '') # Python +``` + +**Responses**: + +CmdOk: Ok. +- type: "cmdOk" +- user_: [User](./TYPES.md#user)? + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + +### APISetContactCustomData + +Set contact custom data. + +*Network usage*: no. + +**Parameters**: +- contactId: int64 +- customData: JSONObject? + +**Syntax**: + +``` +/_set custom @[ ] +``` + +```javascript +'/_set custom @' + contactId + (customData ? ' ' + JSON.stringify(customData) : '') // JavaScript +``` + +```python +'/_set custom @' + str(contactId) + ((' ' + json.dumps(customData)) if customData is not None else '') # Python +``` + +**Responses**: + +CmdOk: Ok. +- type: "cmdOk" +- user_: [User](./TYPES.md#user)? + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + +### APISetUserAutoAcceptMemberContacts + +Set auto-accept member contacts. + +*Network usage*: no. + +**Parameters**: +- userId: int64 +- onOff: bool + +**Syntax**: + +``` +/_set accept member contacts on|off +``` + +```javascript +'/_set accept member contacts ' + userId + ' ' + (onOff ? 'on' : 'off') // JavaScript +``` + +```python +'/_set accept member contacts ' + str(userId) + ' ' + ('on' if onOff else 'off') # Python +``` + +**Responses**: + +CmdOk: Ok. +- type: "cmdOk" +- user_: [User](./TYPES.md#user)? + +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1529,7 +1647,7 @@ Most bots don't need to use these commands, as bot profile can be configured man ### ShowActiveUser -Get active user profile +Get active user profile. *Network usage*: no. @@ -1545,7 +1663,7 @@ ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1554,7 +1672,7 @@ ChatCmdError: Command error. ### CreateActiveUser -Create new user profile +Create new user profile. *Network usage*: no. @@ -1581,7 +1699,7 @@ ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1594,7 +1712,7 @@ ChatCmdError: Command error. ### ListUsers -Get all user profiles +Get all user profiles. *Network usage*: no. @@ -1610,7 +1728,7 @@ UsersList: Users. - type: "usersList" - users: [[UserInfo](./TYPES.md#userinfo)] -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1619,7 +1737,7 @@ ChatCmdError: Command error. ### APISetActiveUser -Set active user profile +Set active user profile. *Network usage*: no. @@ -1647,7 +1765,7 @@ ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1688,7 +1806,7 @@ CmdOk: Ok. - type: "cmdOk" - user_: [User](./TYPES.md#user)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1732,7 +1850,7 @@ UserProfileNoChange: User profile was not changed. - type: "userProfileNoChange" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1771,8 +1889,60 @@ ContactPrefsUpdated: Contact preferences updated. - fromContact: [Contact](./TYPES.md#contact) - toContact: [Contact](./TYPES.md#contact) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) --- + + +## Chat management + +These commands should not be used with CLI-based bots + + +### StartChat + +Start chat controller. + +*Network usage*: no. + +**Parameters**: +- mainApp: bool +- enableSndFiles: bool + +**Syntax**: + +``` +/_start +``` + +**Responses**: + +ChatStarted: Chat started. +- type: "chatStarted" + +ChatRunning: Chat running. +- type: "chatRunning" + +--- + + +### APIStopChat + +Stop chat controller. + +*Network usage*: no. + +**Syntax**: + +``` +/_stop +``` + +**Response**: + +ChatStopped: Chat stopped. +- type: "chatStopped" + +--- diff --git a/bots/api/EVENTS.md b/bots/api/EVENTS.md index a77747482f..d7405ef846 100644 --- a/bots/api/EVENTS.md +++ b/bots/api/EVENTS.md @@ -62,6 +62,11 @@ This file is generated automatically. - [SentGroupInvitation](#sentgroupinvitation) - [GroupLinkConnecting](#grouplinkconnecting) +[Network connection events](#network-connection-events) +- [HostConnected](#hostconnected) +- [HostDisconnected](#hostdisconnected) +- [SubscriptionStatus](#subscriptionstatus) + [Error events](#error-events) - [MessageError](#messageerror) - [ChatError](#chaterror) @@ -685,6 +690,48 @@ Sent when bot joins group via another user link. --- +## Network connection events + + + + +### HostConnected + +Messaging or file server connected + +**Record type**: +- type: "hostConnected" +- protocol: string +- transportHost: string + +--- + + +### HostDisconnected + +Messaging or file server disconnected + +**Record type**: +- type: "hostDisconnected" +- protocol: string +- transportHost: string + +--- + + +### SubscriptionStatus + +Messaging subscription status changed + +**Record type**: +- type: "subscriptionStatus" +- server: string +- subscriptionStatus: [SubscriptionStatus](./TYPES.md#subscriptionstatus) +- connections: [string] + +--- + + ## Error events Bots may log these events for debugging. There will be many error events - this does NOT indicate a malfunction - e.g., they may happen because of bad network connectivity, or because messages may be delivered to deleted chats for a short period of time (they will be ignored). @@ -705,7 +752,7 @@ Message error. ### ChatError -Chat error. +Chat error (only used in WebSockets API). **Record type**: - type: "chatError" diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index ec071e8c3a..4840a2b169 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -153,6 +153,7 @@ This file is generated automatically. - [SndGroupEvent](#sndgroupevent) - [SrvError](#srverror) - [StoreError](#storeerror) +- [SubscriptionStatus](#subscriptionstatus) - [SwitchPhase](#switchphase) - [TimedMessagesGroupPreference](#timedmessagesgrouppreference) - [TimedMessagesPreference](#timedmessagespreference) @@ -765,6 +766,7 @@ Group: - itemTimed: [CITimed](#citimed)? - itemLive: bool? - userMention: bool +- hasLink: bool - deletable: bool - editable: bool - forwardedByMember: int64? @@ -1306,7 +1308,7 @@ Used in API commands. Chat scope can only be passed with groups. ``` ```javascript -chatType.toString() + chatId + (chatScope ? chatScope.toString() : '') // JavaScript +ChatType.cmdString(chatType) + chatId + (chatScope ? GroupChatScope.cmdString(chatScope) : '') // JavaScript ``` ```python @@ -1468,15 +1470,35 @@ LARGE: ## ConnStatus -**Enum type**: -- "new" -- "prepared" -- "joined" -- "requested" -- "accepted" -- "snd-ready" -- "ready" -- "deleted" +**Discriminated union type**: + +New: +- type: "new" + +Prepared: +- type: "prepared" + +Joined: +- type: "joined" + +Requested: +- type: "requested" + +Accepted: +- type: "accepted" + +SndReady: +- type: "sndReady" + +Ready: +- type: "ready" + +Deleted: +- type: "deleted" + +Failed: +- type: "failed" +- connError: string --- @@ -1962,6 +1984,9 @@ Snippet: Secret: - type: "secret" +Small: +- type: "small" + Colored: - type: "colored" - color: [Color](#color) @@ -3591,6 +3616,26 @@ WorkItemError: - errContext: string +--- + +## SubscriptionStatus + +**Discriminated union type**: + +Active: +- type: "active" + +Pending: +- type: "pending" + +Removed: +- type: "removed" +- subError: string + +NoSub: +- type: "noSub" + + --- ## SwitchPhase diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 114e7012c7..db38592617 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -72,7 +72,7 @@ instance IsString ErrorTypeDoc where fromString s = TD s "" -- category name, category description, commands --- inner: constructor, description, responses, errors (ChatErrorType constructors), network usage, syntax +-- inner: constructor, hidden params, description, responses, errors (ChatErrorType constructors), network usage, syntax chatCommandsDocsData :: [(String, String, [(ConsName, [String], Text, [ConsName], [ErrorTypeDoc], Maybe UsesNetwork, Expr)])] chatCommandsDocsData = [ ( "Address commands", @@ -132,7 +132,7 @@ chatCommandsDocsData = "These commands may be used to create connections. Most bots do not need to use them - bot users will connect via bot address with auto-accept enabled.", [ ("APIAddContact", [], "Create 1-time invitation link.", ["CRInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False)), ("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectionLink"), - ("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"), + ("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"), ("Connect", [], "Connect via SimpleX link as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connLink_"), ("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest", "CRChatCmdError"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"), ("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected", "CRChatCmdError"], [], Nothing, "/_reject " <> Param "contactReqId") @@ -142,7 +142,10 @@ chatCommandsDocsData = "Commands to list and delete conversations.", [ ("APIListContacts", [], "Get contacts.", ["CRContactsList", "CRChatCmdError"], [], Nothing, "/_contacts " <> Param "userId"), ("APIListGroups", [], "Get groups.", ["CRGroupsList", "CRChatCmdError"], [], Nothing, "/_groups " <> Param "userId" <> Optional "" (" @" <> Param "$0") "contactId_" <> Optional "" (" " <> Param "$0") "search"), - ("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser", "CRChatCmdError"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode") + ("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser", "CRChatCmdError"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode"), + ("APISetGroupCustomData", [], "Set group custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom #" <> Param "groupId" <> Optional "" (" " <> Json "$0") "customData"), + ("APISetContactCustomData", [], "Set contact custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom @" <> Param "contactId" <> Optional "" (" " <> Json "$0") "customData"), + ("APISetUserAutoAcceptMemberContacts", [], "Set auto-accept member contacts.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set accept member contacts " <> Param "userId" <> " " <> OnOff "onOff") -- ("APIChatItemsRead", [], "Mark items as read.", ["CRItemsReadForChat"], [], Nothing, ""), -- ("APIChatRead", [], "Mark chat as read.", ["CRCmdOk"], [], Nothing, ""), -- ("APIChatUnread", [], "Mark chat as unread.", ["CRCmdOk"], [], Nothing, ""), @@ -163,21 +166,27 @@ chatCommandsDocsData = ), ( "User profile commands", "Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents).", - [ ("ShowActiveUser", [], "Get active user profile", ["CRActiveUser", "CRChatCmdError"], [], Nothing, "/user"), + [ ("ShowActiveUser", [], "Get active user profile.", ["CRActiveUser", "CRChatCmdError"], [], Nothing, "/user"), ( "CreateActiveUser", [], - "Create new user profile", + "Create new user profile.", ["CRActiveUser", "CRChatCmdError"], [TD "CEUserExists" "User or contact with this name already exists", TD "CEInvalidDisplayName" "Invalid user display name"], Nothing, "/_create user " <> Json "newUser" ), - ("ListUsers", [], "Get all user profiles", ["CRUsersList", "CRChatCmdError"], [], Nothing, "/users"), - ("APISetActiveUser", [], "Set active user profile", ["CRActiveUser", "CRChatCmdError"], ["CEChatNotStarted"], Nothing, "/_user " <> Param "userId" <> Optional "" (" " <> Json "$0") "viewPwd"), + ("ListUsers", [], "Get all user profiles.", ["CRUsersList", "CRChatCmdError"], [], Nothing, "/users"), + ("APISetActiveUser", [], "Set active user profile.", ["CRActiveUser", "CRChatCmdError"], ["CEChatNotStarted"], Nothing, "/_user " <> Param "userId" <> Optional "" (" " <> Json "$0") "viewPwd"), ("APIDeleteUser", [], "Delete user profile.", ["CRCmdOk", "CRChatCmdError"], [], Just UNBackground, "/_delete user " <> Param "userId" <> OnOffParam "del_smp" "delSMPQueues" Nothing <> Optional "" (" " <> Json "$0") "viewPwd"), ("APIUpdateProfile", [], "Update user profile.", ["CRUserProfileUpdated", "CRUserProfileNoChange", "CRChatCmdError"], [], Just UNBackground, "/_profile " <> Param "userId" <> " " <> Json "profile"), ("APISetContactPrefs", [], "Configure chat preference overrides for the contact.", ["CRContactPrefsUpdated", "CRChatCmdError"], [], Just UNBackground, "/_set prefs @" <> Param "contactId" <> " " <> Json "preferences") ] + ), + ( "Chat management", + "These commands should not be used with CLI-based bots", + [ ("StartChat", [], "Start chat controller.", ["CRChatStarted", "CRChatRunning"], [], Nothing, "/_start"), + ("APIStopChat", [], "Stop chat controller.", ["CRChatStopped"], [], Nothing, "/_stop") + ] ) ] @@ -341,6 +350,7 @@ undocumentedCommands = "APIGetAppSettings", "APIGetCallInvitations", "APIGetChat", + "APIGetChatContentTypes", "APIGetChatItemInfo", "APIGetChatItems", "APIGetChatItemTTL", @@ -392,11 +402,9 @@ undocumentedCommands = "APISetServerOperators", "APISetUserContactReceipts", "APISetUserGroupReceipts", - "APISetUserAutoAcceptMemberContacts", "APISetUserServers", "APISetUserUIThemes", "APIStandaloneFileInfo", - "APIStopChat", "APIStorageEncryption", "APISuspendChat", "APISwitchContact", @@ -453,7 +461,6 @@ undocumentedCommands = "SetTempFolder", "SetUserProtoServers", "SlowSQLQueries", - "StartChat", "StartRemoteHost", "StopRemoteCtrl", "StopRemoteHost", diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs index a53aa25541..9d43c4e51c 100644 --- a/bots/src/API/Docs/Events.hs +++ b/bots/src/API/Docs/Events.hs @@ -136,6 +136,14 @@ chatEventsDocsData = ], [] ), + ( "Network connection events", + "", + [ ("CEvtHostConnected", "Messaging or file server connected"), + ("CEvtHostDisconnected", "Messaging or file server disconnected"), + ("CEvtSubscriptionStatus", "Messaging subscription status changed") + ], + [] + ), ( "Error events", "Bots may log these events for debugging. \ \There will be many error events - this does NOT indicate a malfunction - \ @@ -143,7 +151,7 @@ chatEventsDocsData = \or because messages may be delivered to deleted chats for a short period of time \ \(they will be ignored).", [ ("CEvtMessageError", ""), - ("CEvtChatError", ""), -- only used in WebSockets API, Haskell code uses Either, with error in Left + ("CEvtChatError", "Chat error (only used in WebSockets API)."), -- Haskell code uses Either, with error in Left ("CEvtChatErrors", "") ], [] @@ -178,10 +186,7 @@ undocumentedEvents = "CEvtCustomChatEvent", "CEvtGroupMemberRatchetSync", "CEvtGroupMemberSwitch", - "CEvtHostConnected", - "CEvtHostDisconnected", "CEvtServiceSubStatus", - "CEvtSubscriptionStatus", "CEvtNewRemoteHost", "CEvtNoMemberContactCreating", "CEvtNtfMessage", diff --git a/bots/src/API/Docs/Generate.hs b/bots/src/API/Docs/Generate.hs index 334ad93bad..99886bf222 100644 --- a/bots/src/API/Docs/Generate.hs +++ b/bots/src/API/Docs/Generate.hs @@ -73,7 +73,7 @@ syntaxText :: TypeAndFields -> Expr -> Text syntaxText r syntax = "\n**Syntax**:\n" <> "\n```\n" <> docSyntaxText r syntax <> "\n```\n" - <> (if isConst syntax then "" else "\n```javascript\n" <> jsSyntaxText False r syntax <> " // JavaScript\n```\n") + <> (if isConst syntax then "" else "\n```javascript\n" <> jsSyntaxText False "" r syntax <> " // JavaScript\n```\n") <> (if isConst syntax then "" else "\n```python\n" <> pySyntaxText r syntax <> " # Python\n```\n") camelToSpace :: String -> String diff --git a/bots/src/API/Docs/Generate/TypeScript.hs b/bots/src/API/Docs/Generate/TypeScript.hs index b69635086e..c3049c100d 100644 --- a/bots/src/API/Docs/Generate/TypeScript.hs +++ b/bots/src/API/Docs/Generate/TypeScript.hs @@ -49,7 +49,7 @@ commandsCodeText = <> "}\n\n" <> ("export namespace " <> T.pack constrName <> " {\n") <> (" export type Response = " <> constrsCode " " "CR" (("CR." <> ) . T.pack . fstToUpper . memberTag) (map responseType responses)) - <> (if syntax == "" then "" else funcCode APITypeDef {typeName' = constrName, typeDef = ATDRecord params} syntax) + <> (if syntax == "" then "" else funcCode APITypeDef {typeName' = constrName, typeDef = ATDRecord params} "T." syntax) <> "}\n" where constrName = fstToUpper tag @@ -86,7 +86,7 @@ typesCodeText = ("// API Types\n// " <> autoGenerated <> "\n") <> foldMap typeCo "ConnectionMode" -> T.pack $ map toUpper tag "FileProtocol" -> T.pack $ map toUpper tag _ -> T.replace "-" "_" $ T.pack $ fstToUpper tag - namespaceFuncCode = "\nexport namespace " <> name' <> " {" <> funcCode td typeSyntax <> "}\n" + namespaceFuncCode = "\nexport namespace " <> name' <> " {" <> funcCode td "" typeSyntax <> "}\n" typeDefCode = case typeDef of ATDRecord fields -> ("\nexport interface " <> name' <> " {\n") @@ -107,7 +107,7 @@ unionTypeCode unionNamespace typesNamespace td@APITypeDef {typeName' = name} cs <> (" export type Tag = " <> constrsCode " " name' constrTag (L.toList cs) <> "\n") <> (" interface Interface {\n type: Tag\n }\n") <> foldMap constrType cs - <> (if cmdSyntax == "" then "" else funcCode td cmdSyntax) + <> (if cmdSyntax == "" then "" else funcCode td typesNamespace cmdSyntax) <> "}\n" where name' = T.pack name @@ -128,9 +128,9 @@ constrsCode indent name' constr cs line = T.intercalate " | " cs' cs' = map constr cs -funcCode :: APITypeDef -> Expr -> Text -funcCode td@APITypeDef {typeName' = name, typeDef} cmdSyntax = - "\n export function cmdString(" <> param <> ": " <> T.pack name <> "): string {\n return " <> jsSyntaxText True (name, self : typeFields) cmdSyntax <> "\n }\n" +funcCode :: APITypeDef -> String -> Expr -> Text +funcCode td@APITypeDef {typeName' = name, typeDef} typeNamespace cmdSyntax = + "\n export function cmdString(" <> param <> ": " <> T.pack name <> "): string {\n return " <> jsSyntaxText True typeNamespace (name, self : typeFields) cmdSyntax <> "\n }\n" where param = if hasParams cmdSyntax then "self" else "_self" self = APIRecordField "self" (ATDef td) diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 154d44b6c2..60fe129cdb 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -51,8 +51,11 @@ chatResponsesDocsData = ("CRChatItemReaction", "Message reaction"), ("CRChatItemUpdated", "Message updated"), ("CRChatItemsDeleted", "Messages deleted"), + ("CRChatRunning", ""), + ("CRChatStarted", ""), + ("CRChatStopped", ""), ("CRCmdOk", "Ok"), - ("CRChatCmdError", "Command error"), -- only used in WebSockets API, Haskell code uses Either, with error in Left + ("CRChatCmdError", "Command error (only used in WebSockets API)"), -- Haskell code uses Either, with error in Left ("CRConnectionPlan", "Connection link information"), ("CRContactAlreadyExists", ""), ("CRContactConnectionDeleted", "Connection deleted"), @@ -121,15 +124,13 @@ undocumentedResponses = "CRBroadcastSent", "CRCallInvitations", "CRChatCleared", + "CRChatContentTypes", "CRChatHelp", "CRChatItemId", "CRChatItemInfo", "CRChatItems", "CRChatItemTTL", - "CRChatRunning", "CRChats", - "CRChatStarted", - "CRChatStopped", "CRConnectionsDiff", "CRChatTags", "CRConnectionAliasUpdated", diff --git a/bots/src/API/Docs/Syntax.hs b/bots/src/API/Docs/Syntax.hs index 83fa2bf6a2..f96ec03b02 100644 --- a/bots/src/API/Docs/Syntax.hs +++ b/bots/src/API/Docs/Syntax.hs @@ -99,8 +99,8 @@ withOptBoolParam r param p f = (ATOptional (ATPrim (PT TBool))) -> f True _ -> paramError r param p "is not [optional] boolean" -jsSyntaxText :: Bool -> TypeAndFields -> Expr -> Text -jsSyntaxText useSelf r = T.replace "' + '" "" . T.pack . go Nothing True +jsSyntaxText :: Bool -> String -> TypeAndFields -> Expr -> Text +jsSyntaxText useSelf typeNamespace r = T.replace "' + '" "" . T.pack . go Nothing True where go param top = \case Concat exs -> intercalate " + " $ map (go param False) $ L.toList exs @@ -112,7 +112,7 @@ jsSyntaxText useSelf r = T.replace "' + '" "" . T.pack . go Nothing True _ -> paramName' useSelf param p where toStringSyntax (APITypeDef typeName _) - | typeHasSyntax typeName = paramName' useSelf param p <> ".toString()" + | typeHasSyntax typeName = typeNamespace <> typeName <> ".cmdString(" <> paramName' useSelf param p <> ")" | otherwise = paramName' useSelf param p Optional exN exJ p -> open <> n <> " ? " <> go (Just p) False exJ <> " : " <> nothing <> close where diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 73e427fa03..21970ce419 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -176,6 +176,7 @@ ciQuoteType = updateRecord (RecordTypeInfo name fields) = RecordTypeInfo name $ map optChatDir fields in st {recordTypes = map updateRecord records} -- need to map even though there is one constructor in this type +-- type info, JSON encoding, constructor prefix, removed constructors, string encoding for commands, description chatTypesDocsData :: [(SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text)] chatTypesDocsData = [ ((sti @(Chat 'CTDirect)) {typeName = "AChat"}, STRecord, "", [], "", ""), @@ -239,7 +240,7 @@ chatTypesDocsData = (sti @ConnectionErrorType, STUnion, "", [], "", ""), (sti @ConnectionMode, (STEnum' $ take 3 . consLower "CM"), "", [], "", ""), (sti @ConnectionPlan, STUnion, "CP", [], "", ""), - (sti @ConnStatus, (STEnum' $ consSep "Conn" '-'), "", [], "", ""), + (sti @ConnStatus, STUnion, "Conn", [], "", ""), (sti @ConnType, (STEnum' $ consSep "Conn" '_'), "", [], "", ""), (sti @Contact, STRecord, "", [], "", ""), (sti @ContactAddressPlan, STUnion, "CAP", [], "", ""), @@ -332,6 +333,7 @@ chatTypesDocsData = (sti @SndGroupEvent, STUnion, "SGE", [], "", ""), (sti @SrvError, STUnion, "SrvErr", [], "", ""), (sti @StoreError, STUnion, "SE", [], "", ""), + (sti @SubscriptionStatus, STUnion, "SS", [], "", ""), (sti @SwitchPhase, STEnum, "SP", [], "", ""), (sti @TimedMessagesGroupPreference, STRecord, "", [], "", ""), (sti @TimedMessagesPreference, STRecord, "", [], "", ""), @@ -522,6 +524,7 @@ deriving instance Generic SndFileTransfer deriving instance Generic SndGroupEvent deriving instance Generic SrvError deriving instance Generic StoreError +deriving instance Generic SubscriptionStatus deriving instance Generic SwitchPhase deriving instance Generic TimedMessagesGroupPreference deriving instance Generic TimedMessagesPreference diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index df43374ffa..a70de72d01 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -194,6 +194,7 @@ toTypeInfo tr = primitiveToLower st@(ST t ps) = let t' = fstToLower t in if t' `elem` primitiveTypes then ST t' ps else st stringTypes = [ "AConnectionLink", + "AProtocolType", "AgentConnId", "AgentInvId", "AgentRcvFileId", @@ -212,6 +213,7 @@ toTypeInfo tr = "ProtocolServer", "SbKey", "SharedMsgId", + "TransportHost", "UIColor", "UserPwd", "XContactId" diff --git a/cabal.project b/cabal.project index 6989ba322b..35a6e3265f 100644 --- a/cabal.project +++ b/cabal.project @@ -2,6 +2,15 @@ packages: . -- packages: . ../simplexmq -- packages: . ../simplexmq ../direct-sqlcipher ../sqlcipher-simple +-- uncomment two sections below to run tests with coverage +-- package * +-- coverage: True +-- library-coverage: True + +-- package attoparsec +-- coverage: False +-- library-coverage: False + index-state: 2023-12-12T00:00:00Z package cryptostore @@ -12,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: db4b27e88a95af5b295d393b4c4483ffd220fafb + tag: 5f08457b7e5cd6e42f03a3d5bcabd716afd8b91c source-repository-package type: git diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f163335388..6ae5418d0a 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -7,6 +7,31 @@ revision: 25.07.2025 # Contributing guide +## Focus on user problems + +We do not make code changes to improve code - any change must address a specific user problem or request. + +## Discuss the plans as early as possible + +Please discuss the problem you want to solve and your detailed implementation plan with the project team prior to contributing, to avoid wasted time and additional changes. Acceptance of your contribution depends on your willingness and ability to iterate the proposed contribution to achieve the required quality level, coding style, test coverage, and alignment with user requirements as they are understood by the project team. + +## Follow project structure, coding style and approaches + +./contributing/PROJECT.md has information about the structure of this `simplex-chat` repository. + +./contributing/CODE.md has details about general requirements common for `simplexmq` and `simplex-chat` repositories. + +This files can be used with LLM prompts, e.g. if you use Claude Code you can create CLAUDE.md file in project root importing content from these files: + +```markdown +@README.md +@docs/CONTRIBUTING.md +@docs/contributing/PROJECT.md +@docs/contributing/CODE.md +``` + +For Android/Desktop and iOS apps you can additionally import `apps/multiplatform/README.md` and `apps/ios/README.md`. + ## Compiling with SQLCipher encryption enabled Add `cabal.project.local` to project root with the location of OpenSSL headers and libraries and flag setting encryption mode: diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index b9889fe7a7..f0e9466c61 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -4,7 +4,6 @@ permalink: /downloads/index.html revision: 09.09.2024 --- -| Updated 09.09.2024 | Languages: EN | # Download SimpleX apps You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). diff --git a/docs/TRANSPARENCY.md b/docs/TRANSPARENCY.md index bd0dcabb53..c3e74d6b28 100644 --- a/docs/TRANSPARENCY.md +++ b/docs/TRANSPARENCY.md @@ -1,18 +1,18 @@ --- title: Transparency Reports permalink: /transparency/index.html -revision: 15.01.2025 +revision: 09.02.2026 --- # Transparency Reports -**Updated**: Jan 15, 2025 +**Updated**: Feb 09, 2026 SimpleX Chat Ltd. is a company registered in the UK – it develops communication software enabling users to operate and communicate via SimpleX network, without user profile identifiers of any kind, and without having their data hosted by any network infrastructure operators. This page will include any and all reports on requests for user data. -*To date, we received none*. +In 2025 we received 12 requests from law enforcement of different countries. No responsive information was identified/provided. In 2024 we received enquiries from several law enforcement agencies seeking information on our procedures for handling data requests. We responded by noting that we operate under the UK law and will consider such requests pursuant to UK law. @@ -29,6 +29,6 @@ Our objective is to consistently ensure that no user data and absolute minimum o - Trail of Bits, SimpleX cryptography and networking, [October 2022](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). - Trail of Bits, the cryptographic review of SimpleX protocols design, [July 2024](../blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md). -Have a more specific question? Reach out to us via [SimpleX Chat](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) or via email [chat@simplex.chat](mailto:chat@simplex.chat). +Have a more specific question? Reach out to us via [SimpleX Chat](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw) or via email [chat@simplex.chat](mailto:chat@simplex.chat). For any sensitive questions please use SimpleX Chat or encrypted email messages using the key for this address from [keys.openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) (its fingerprint is `FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC`) and make your key available for a secure reply. diff --git a/docs/WHY.md b/docs/WHY.md new file mode 100644 index 0000000000..e1582984df --- /dev/null +++ b/docs/WHY.md @@ -0,0 +1,19 @@ +# Why we are building SimpleX Network + +You were born without an account. + +Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature — it was the way of life. + +Then we moved online, and every platform asked for a piece of you — your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way — telephone, email, messengers, social media. It seemed the only way possible. + +There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + +Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it — you are sovereign. + +Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + +The oldest human freedom — to speak to another person without being watched — built on infrastructure that cannot betray it. + +Because we destroyed the power to know who you are. So that your power can never be taken. + +Be free in your network. diff --git a/docs/contributing/CODE.md b/docs/contributing/CODE.md new file mode 100644 index 0000000000..7ae6d176ac --- /dev/null +++ b/docs/contributing/CODE.md @@ -0,0 +1,94 @@ +# Coding and building + +This file provides guidance on coding style and approaches and on building the code. + +## Code Style, Formatting and Approaches + +The project uses **fourmolu** for Haskell code formatting. Configuration is in `fourmolu.yaml`. + +**Key formatting rules:** +- 2-space indentation +- Trailing function arrows, commas, and import/export style +- Record brace without space: `{field = value}` +- Single newline between declarations +- Never use unicode symbols +- Inline `let` style with right-aligned `in` + +**Format code before committing:** + +```bash +# Format a single file +fourmolu -i src/Simplex/Messaging/Protocol.hs +``` + +Some files that use CPP language extension cannot be formatted as a whole, so individual code fragments need to be formatted. + +**Follow existing code patterns:** +- Match the style of surrounding code +- Use qualified imports with short aliases (e.g., `import qualified Data.ByteString.Char8 as B`) +- Use record syntax for types with multiple fields +- Prefer explicit pattern matching over partial functions + +**Comments policy:** +- Avoid redundant comments that restate what the code already says +- Only comment on non-obvious design decisions or tricky implementation details +- Function names and type signatures should be self-documenting +- Do not add comments like "wire format encoding" (Encoding class is always wire format) or "check if X" when the function name already says that +- Assume a competent Haskell reader + +**Diff and refactoring:** +- Avoid unnecessary changes and code movements +- Never do refactoring unless it substantially reduces cost of solving the current problem, including the cost of refactoring +- Aim to minimize the code changes - do what is minimally required to solve users' problems + +**Document and code structure:** +- **Never move existing code or sections around** - add new content at appropriate locations without reorganizing existing structure. +- When adding new sections to documents, continue the existing numbering scheme. +- Minimize diff size - prefer small, targeted changes over reorganization. + +**Code analysis and review:** +- 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. +- Read implementations of called functions, not just signatures — if duplication involves a called function, check whether decomposing it resolves the duplication. +- Do not save time on analysis. Read every function in the data flow even when the interface seems clear — wrong assumptions about internals are the main source of missed bugs. + +### Haskell Extensions +- `StrictData` enabled by default +- Use STM for safe concurrency +- Assume concurrency in PostgreSQL queries +- Comprehensive warning flags with strict pattern matching + +## Build Commands + +```bash +# Standard build +cabal build + +# Fast build +cabal build --ghc-options -O0 + +# Build specific executables +cabal build exe:simplex-chat + +# Build with PostgreSQL client support +cabal build -fclient_postgres + +# Client-only library build (no server code) +cabal build -fclient_library + +# Find binary location +cabal list-bin exe:simplex-chat +``` + +### Cabal Flags + +- `swift`: Enable Swift JSON format +- `client_library`: Build without server code +- `client_postgres`: Use PostgreSQL instead of SQLite for agent persistence +- `server_postgres`: PostgreSQL support for server queue/notification store + +## External Dependencies + +Custom forks specified in `cabal.project`: +- `aeson`, `hs-socks` (SimpleX forks) +- `direct-sqlcipher`, `sqlcipher-simple` (encrypted SQLite) +- `warp`, `warp-tls` (HTTP server) diff --git a/docs/contributing/PROJECT.md b/docs/contributing/PROJECT.md new file mode 100644 index 0000000000..3f7e6e0e54 --- /dev/null +++ b/docs/contributing/PROJECT.md @@ -0,0 +1,92 @@ +# SimpleX-Chat repository + +This file provides guidance on the project structure to help working with code in this repository. + +## Project Overview + +SimpleX Chat is a decentralized, privacy-focused messaging platform with **no user identifiers**. Users are identified by disposable, per-connection message queue addresses instead of any persistent ID. + +**Key components:** +- **Core library** (Haskell): `src/Simplex/Chat/` - chat protocol, controller, message handling, database storage +- **Terminal CLI**: `src/Simplex/Chat/Terminal/` +- **Mobile apps**: `apps/multiplatform/` (Kotlin Compose Multiplatform for Android/Desktop) +- **iOS app**: `apps/ios/` (SwiftUI) +- **Bot framework**: `bots/`, `packages/simplex-chat-nodejs/` +- **Website**: `website/` (11ty + Tailwind CSS) + +## Specifications + +Chat protocol: docs/protocol/simplex-chat.md + +RFCs: docs/rfcs + +## Core Haskell Modules + +- `Controller.hs` - Main chat controller, orchestrates all chat operations +- `Types.hs` - Core type definitions (contacts, groups, messages, profiles) +- `Protocol.hs` - Chat protocol encoding/decoding +- `Messages.hs` - Message types and handling +- `Store/` - Database layer (SQLite by default, PostgreSQL optional) + - `Messages.hs` - Message storage + - `Groups.hs` - Group storage + - `Direct.hs` - Direct chat storage + - `Connections.hs` - Connection management +- `Mobile.hs` - FFI interface +- `Library/` - commands and events processing + - `Commands.hs` - all supported chat commands. They can be sent via CLI or via FFI functions. + - `Subscriber.hs` - processing events from the [agent](../../../simplexmq/src/Simplex/Messaging/Agent/Protocol.hs) + +### Database Migrations + +SQLite migrations are in `src/Simplex/Chat/Store/SQLite/Migrations/`. PostgreSQL migrations are in `src/Simplex/Chat/Store/Postgres/Migrations/`. Each migration is a separate module named `M{YYYYMMDD}_{description}.hs`. + +**Important:** The `chat_schema.sql` files in both migration directories are **auto-generated by tests** - do not edit them directly. They reflect the final schema state after all migrations are applied. + +When creating a new migration: +1. Create the migration module (e.g., `M20260122_feature.hs`) +2. Register it in the corresponding `Migrations.hs` file +3. Add the module to `simplex-chat.cabal` under exposed-modules +4. Schema files will be updated automatically when tests are run + +### Test Structure + +Tests are in `tests/`: +- `ChatTests/` - Integration tests (Direct, Groups, Files, Profiles) +- `ProtocolTests.hs` - Protocol encoding/decoding tests +- `JSONTests.hs` - JSON serialization tests +- `Bots/` - Bot-specific tests + +## Key Dependencies + +The project uses several custom forks managed via `cabal.project`: +- `simplexmq` - Core SimpleX Messaging Protocol (separate [repo](../../../simplexmq/README.md)) +- `direct-sqlcipher` - SQLite with encryption +- `aeson` - JSON serialization (custom fork) + +## Android/Desktop (Kotlin Multiplatform) + +```bash +cd apps/multiplatform + +# Build Android debug APK +./gradlew assembleDebug + +# Build desktop +./gradlew :desktop:packageDistributionForCurrentOS + +# Run Android tests +./gradlew connectedAndroidTest +``` + +### iOS + +Open `apps/ios/SimpleX.xcodeproj` in Xcode. Build targets include the main app, Share Extension, and Notification Service Extension. + +### Website + +```bash +cd website +npm install +npm run start # Dev server +npm run build # Production build +``` diff --git a/docs/rfcs/2026-02-10-member-support-voice.md b/docs/rfcs/2026-02-10-member-support-voice.md new file mode 100644 index 0000000000..52285d1514 --- /dev/null +++ b/docs/rfcs/2026-02-10-member-support-voice.md @@ -0,0 +1,212 @@ +# Voice messages in member support scope + +## Table of contents + +1. Executive summary +2. Problem +3. High-level design +4. Detailed implementation plan + +## 1. Executive summary + +Allow voice messages from host/admin during the approval phase (member pending) regardless of group voice settings, gated behind chat protocol version 17. This enables the directory bot to send voice captchas in groups that prohibit voice messages. Old clients that don't support this exemption will receive text/image captchas instead. + +## 2. Problem + +The directory bot sends voice captchas to joining members via the member support scope (`GCSMemberSupport`). However, `prohibitedGroupContent` (Internal.hs:338) blocks voice messages when the group disables voice — with no scope exemption: + +```haskell +| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice +``` + +Other content types (files, reports, simplex links) already have `isNothing scopeInfo` guards that exempt them in member support scope. Voice does not. + +This means voice captchas fail in the majority of real groups that prohibit voice messages. The check runs on both sender side (Commands.hs:3856) and recipient side (Subscriber.hs:1738), so both the bot and the joining member reject voice in these groups. + +## 3. High-level design + +1. **Protocol version 17** (`memberSupportVoiceVersion`): gates the `prohibitedGroupContent` exemption for host voice during the approval phase. + +2. **Core library change** (Internal.hs): exempt voice in `prohibitedGroupContent` when sender is admin+ (host) AND the member is in the approval phase (pending status). Voice is NOT generally allowed in member support scope — only during approval, only from host. + +3. **Directory bot change** (Service.hs): check member's protocol version and group voice settings before offering or sending voice captcha. Fall back to text/image captcha for old clients in voice-disabled groups. + +## 4. Detailed implementation plan + +### 4.1. Protocol.hs — add version 17 + +**File:** `src/Simplex/Chat/Protocol.hs` + +Add to version history comment (after line 79): + +``` +-- 17 - allow host voice messages during member approval regardless of group voice setting (2026-02-10) +``` + +Update `currentChatVersion` (line 85): + +```haskell +currentChatVersion = VersionChat 17 +``` + +Add version constant (after `shortLinkDataVersion`, line 146): + +```haskell +-- support host voice messages during member approval regardless of group voice setting +memberSupportVoiceVersion :: VersionChat +memberSupportVoiceVersion = VersionChat 17 +``` + +### 4.2. Internal.hs — exempt host voice during approval phase + +**File:** `src/Simplex/Chat/Library/Internal.hs` + +Change function header (line 337) to bind sender's role and full membership: + +```haskell +prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole = userRole}} m@GroupMember {memberRole = senderRole} scopeInfo mc ft file_ sent +``` + +Change line 338 from: + +```haskell + | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice +``` + +to: + +```haskell + | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) && not hostApprovalVoice = Just GFVoice +``` + +Add to the `where` clause: + +```haskell + hostApprovalVoice = senderRole >= GRAdmin && inApprovalPhase + inApprovalPhase = case scopeInfo of + Just (GCSIMemberSupport (Just scopeMem)) -> memberPending scopeMem + Just (GCSIMemberSupport Nothing) -> memberPending mem + Nothing -> False +``` + +Note: `memberPending` returns True for both `GSMemPendingApproval` and `GSMemPendingReview`. The exemption applies to both phases — the member hasn't been fully admitted in either state. + +**Why two cases for `inApprovalPhase`:** + +- **Sender side** (bot sending via Commands.hs:3856): `scopeInfo = GCSIMemberSupport (Just pendingMember)` — the scope contains the pending member being supported. `memberPending pendingMember` checks their status. +- **Receiver side** (member receiving via Subscriber.hs:1738): `scopeInfo = GCSIMemberSupport Nothing` — `Nothing` means the member's own support conversation (constructed by `mkGroupSupportChatInfo` in Internal.hs:1535). `memberPending mem` checks the local user's (receiving member's) status. + +**Behavior matrix:** + +| Scenario | `hostApprovalVoice` | Voice allowed? | +|----------|---------------------|----------------| +| Host → pending member, voice disabled | True | Yes (new) | +| Host → approved member in support, voice disabled | False (`memberPending` = False) | No | +| Pending member → host, voice disabled | False (`senderRole` < GRAdmin) | No | +| Anyone outside support scope, voice disabled | False (`inApprovalPhase` = False) | No | +| Any sender, voice enabled | N/A (`groupFeatureMemberAllowed` = True) | Yes (existing) | + +**Version gating:** Old clients (< v17) don't have this exemption. On the sender side this is handled by the bot (4.3). On the recipient side: + +- Old recipient + voice-disabled group: recipient rejects the voice message (shows "Voice messages: received, prohibited") +- This is why the bot must check the member's version before sending voice + +### 4.3. Service.hs — version-aware voice captcha logic + +**File:** `apps/simplex-directory-service/src/Directory/Service.hs` + +#### 4.3.1. Add import + +Add `memberSupportVoiceVersion` to the `Protocol` import: + +```haskell +import Simplex.Chat.Protocol (MsgContent (..), memberSupportVoiceVersion) +``` + +#### 4.3.2. Add helper predicate + +Add a helper in the `directoryService` `where` block (same scope as `sendMemberCaptcha`, `sendVoiceCaptcha`, etc., where `opts` is in scope): + +```haskell +canSendVoiceCaptcha :: GroupInfo -> GroupMember -> Bool +canSendVoiceCaptcha gInfo m = + isJust (voiceCaptchaGenerator opts) + && (groupFeatureUserAllowed SGFVoice gInfo || supportsVersion m memberSupportVoiceVersion) +``` + +Logic: +- Voice captcha generator must be configured +- AND either the group allows voice for the bot/host (any client version works — old clients accept voice from permitted senders) OR the member's client supports v17 (exemption applies on receive side) + +Note: `groupFeatureUserAllowed` checks if the bot (group owner) is permitted to send voice. This is what the recipient's `prohibitedGroupContent` checks — it validates the *sender's* permission (`m` parameter = sender's GroupMember), not the recipient's. Using `groupFeatureMemberAllowed SGFVoice m gInfo` (joining member) would be wrong: it would incorrectly block voice captcha in groups with role-based voice settings (e.g., "admins only"). + +#### 4.3.3. Update `dePendingMember` hint text (line 572) + +Change from: + +```haskell +<> if isJust (voiceCaptchaGenerator opts) then "\nSend /audio to receive a voice captcha." else "" +``` + +to: + +```haskell +<> if canSendVoiceCaptcha g m then "\nSend /audio to receive a voice captcha." else "" +``` + +This hides the `/audio` hint when voice captcha cannot be delivered. + +#### 4.3.4. Update `dePendingMemberMsg` `/audio` handling (lines 644-649) + +When a member sends `/audio`, check `canSendVoiceCaptcha` before switching mode. If voice captcha is not possible, reply with an upgrade message: + +```haskell +| isAudioCmd -> + if canSendVoiceCaptcha g m + then case captchaMode of + CMText -> do + atomically $ TM.insert gmId pc {captchaMode = CMAudio} $ pendingCaptchas env + sendVoiceCaptcha sendRef (T.unpack captchaText) + CMAudio -> + sendComposedMessages_ cc sendRef [(Just ciId, MCText audioAlreadyEnabled)] + else sendComposedMessages_ cc sendRef [(Just ciId, MCText voiceCaptchaUnavailable)] +``` + +#### 4.3.5. Add message constant + +```haskell +voiceCaptchaUnavailable :: Text +voiceCaptchaUnavailable = "Voice captcha is not available - please update SimpleX Chat to v6.5+ or use text captcha." +``` + +#### 4.3.6. Update `dePendingMemberMsg` no-captcha `/audio` path (lines 640-642) + +Same check for the case when no pending captcha exists yet: + +```haskell +Nothing -> + if isAudioCmd && canSendVoiceCaptcha g m + then sendMemberCaptcha g m (Just ciId) noCaptcha 0 CMAudio + else if isAudioCmd + then sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [(Just ciId, MCText voiceCaptchaUnavailable)] + else let mode = CMText + in sendMemberCaptcha g m (Just ciId) noCaptcha 0 mode +``` + +### 4.4. Tests + +**File:** `tests/Bots/DirectoryTests.hs` + +Update existing audio captcha tests to cover: +1. Group with voice enabled + any client version: `/audio` works (existing behavior) +2. Group with voice disabled + member version >= 17: `/audio` works +3. Group with voice disabled + member version < 17: `/audio` shows unavailable message, hint is hidden + +### 4.5. Changes summary + +| File | Change | Lines affected | +|------|--------|----------------| +| `Protocol.hs` | Add v17 constant, bump `currentChatVersion` | ~4 lines added | +| `Internal.hs` | Exempt host voice during approval phase | ~6 lines modified/added | +| `Service.hs` | Version-aware voice captcha logic | ~15 lines modified/added | +| `DirectoryTests.hs` | Test coverage for version gating | TBD | diff --git a/packages/simplex-chat-client/types/typescript/README.md b/packages/simplex-chat-client/types/typescript/README.md index ad13a5d76e..e30cf0d0c3 100644 --- a/packages/simplex-chat-client/types/typescript/README.md +++ b/packages/simplex-chat-client/types/typescript/README.md @@ -2,7 +2,7 @@ This TypeScript library provides auto-generated types for bots API: commands and responses, events and all types they use. -It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) library that uses WebSockets interface of SimpleX Chat CLI. +It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) Node.js library. [API reference](https://github.com/simplex-chat/simplex-chat/tree/stable/bots). diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json index 1bf593b483..a135b286c2 100644 --- a/packages/simplex-chat-client/types/typescript/package.json +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/types", - "version": "0.1.0", + "version": "0.3.0", "description": "TypeScript types for SimpleX Chat bot libraries", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -35,7 +35,7 @@ "bugs": { "url": "https://github.com/simplex-chat/simplex-chat/issues" }, - "homepage": "https://github.com/simplex-chat/simplex-chat#readme", + "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/types/typescript#readme", "dependencies": { "typescript": "^5.9.2" } diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index de6a7a7ce1..edeabe7837 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -96,7 +96,7 @@ export namespace APISendMessages { export type Response = CR.NewChatItems | CR.ChatCmdError export function cmdString(self: APISendMessages): string { - return '/_send ' + self.sendRef.toString() + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + ' json ' + JSON.stringify(self.composedMessages) + return '/_send ' + T.ChatRef.cmdString(self.sendRef) + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + ' json ' + JSON.stringify(self.composedMessages) } } @@ -113,7 +113,7 @@ export namespace APIUpdateChatItem { export type Response = CR.ChatItemUpdated | CR.ChatItemNotChanged | CR.ChatCmdError export function cmdString(self: APIUpdateChatItem): string { - return '/_update item ' + self.chatRef.toString() + ' ' + self.chatItemId + (self.liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(self.updatedMessage) + return '/_update item ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemId + (self.liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(self.updatedMessage) } } @@ -129,7 +129,7 @@ export namespace APIDeleteChatItem { export type Response = CR.ChatItemsDeleted | CR.ChatCmdError export function cmdString(self: APIDeleteChatItem): string { - return '/_delete item ' + self.chatRef.toString() + ' ' + self.chatItemIds.join(',') + ' ' + self.deleteMode + return '/_delete item ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemIds.join(',') + ' ' + self.deleteMode } } @@ -161,7 +161,7 @@ export namespace APIChatItemReaction { export type Response = CR.ChatItemReaction | CR.ChatCmdError export function cmdString(self: APIChatItemReaction): string { - return '/_reaction ' + self.chatRef.toString() + ' ' + self.chatItemId + ' ' + (self.add ? 'on' : 'off') + ' ' + JSON.stringify(self.reaction) + return '/_reaction ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemId + ' ' + (self.add ? 'on' : 'off') + ' ' + JSON.stringify(self.reaction) } } @@ -450,7 +450,7 @@ export namespace APIConnectPlan { } } -// Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link +// Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link. // Network usage: interactive. export interface APIConnect { userId: number // int64 @@ -462,7 +462,7 @@ export namespace APIConnect { export type Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError export function cmdString(self: APIConnect): string { - return '/_connect ' + self.userId + (self.preparedLink_ ? ' ' + self.preparedLink_.toString() : '') + return '/_connect ' + self.userId + (self.preparedLink_ ? ' ' + T.CreatedConnLink.cmdString(self.preparedLink_) : '') } } @@ -553,14 +553,59 @@ export namespace APIDeleteChat { export type Response = CR.ContactDeleted | CR.ContactConnectionDeleted | CR.GroupDeletedUser | CR.ChatCmdError export function cmdString(self: APIDeleteChat): string { - return '/_delete ' + self.chatRef.toString() + ' ' + self.chatDeleteMode.toString() + return '/_delete ' + T.ChatRef.cmdString(self.chatRef) + ' ' + T.ChatDeleteMode.cmdString(self.chatDeleteMode) + } +} + +// Set group custom data. +// Network usage: no. +export interface APISetGroupCustomData { + groupId: number // int64 + customData?: object +} + +export namespace APISetGroupCustomData { + export type Response = CR.CmdOk | CR.ChatCmdError + + export function cmdString(self: APISetGroupCustomData): string { + return '/_set custom #' + self.groupId + (self.customData ? ' ' + JSON.stringify(self.customData) : '') + } +} + +// Set contact custom data. +// Network usage: no. +export interface APISetContactCustomData { + contactId: number // int64 + customData?: object +} + +export namespace APISetContactCustomData { + export type Response = CR.CmdOk | CR.ChatCmdError + + export function cmdString(self: APISetContactCustomData): string { + return '/_set custom @' + self.contactId + (self.customData ? ' ' + JSON.stringify(self.customData) : '') + } +} + +// Set auto-accept member contacts. +// Network usage: no. +export interface APISetUserAutoAcceptMemberContacts { + userId: number // int64 + onOff: boolean +} + +export namespace APISetUserAutoAcceptMemberContacts { + export type Response = CR.CmdOk | CR.ChatCmdError + + export function cmdString(self: APISetUserAutoAcceptMemberContacts): string { + return '/_set accept member contacts ' + self.userId + ' ' + (self.onOff ? 'on' : 'off') } } // User profile commands // Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents). -// Get active user profile +// Get active user profile. // Network usage: no. export interface ShowActiveUser { } @@ -573,7 +618,7 @@ export namespace ShowActiveUser { } } -// Create new user profile +// Create new user profile. // Network usage: no. export interface CreateActiveUser { newUser: T.NewUser @@ -587,7 +632,7 @@ export namespace CreateActiveUser { } } -// Get all user profiles +// Get all user profiles. // Network usage: no. export interface ListUsers { } @@ -600,7 +645,7 @@ export namespace ListUsers { } } -// Set active user profile +// Set active user profile. // Network usage: no. export interface APISetActiveUser { userId: number // int64 @@ -660,3 +705,34 @@ export namespace APISetContactPrefs { return '/_set prefs @' + self.contactId + ' ' + JSON.stringify(self.preferences) } } + +// Chat management +// These commands should not be used with CLI-based bots + +// Start chat controller. +// Network usage: no. +export interface StartChat { + mainApp: boolean + enableSndFiles: boolean +} + +export namespace StartChat { + export type Response = CR.ChatStarted | CR.ChatRunning + + export function cmdString(_self: StartChat): string { + return '/_start' + } +} + +// Stop chat controller. +// Network usage: no. +export interface APIStopChat { +} + +export namespace APIStopChat { + export type Response = CR.ChatStopped + + export function cmdString(_self: APIStopChat): string { + return '/_stop' + } +} diff --git a/packages/simplex-chat-client/types/typescript/src/events.ts b/packages/simplex-chat-client/types/typescript/src/events.ts index d7a0419bbe..cb6ba85c8b 100644 --- a/packages/simplex-chat-client/types/typescript/src/events.ts +++ b/packages/simplex-chat-client/types/typescript/src/events.ts @@ -46,6 +46,9 @@ export type ChatEvent = | CEvt.JoinedGroupMemberConnecting | CEvt.SentGroupInvitation | CEvt.GroupLinkConnecting + | CEvt.HostConnected + | CEvt.HostDisconnected + | CEvt.SubscriptionStatus | CEvt.MessageError | CEvt.ChatError | CEvt.ChatErrors @@ -94,6 +97,9 @@ export namespace CEvt { | "joinedGroupMemberConnecting" | "sentGroupInvitation" | "groupLinkConnecting" + | "hostConnected" + | "hostDisconnected" + | "subscriptionStatus" | "messageError" | "chatError" | "chatErrors" @@ -411,6 +417,25 @@ export namespace CEvt { hostMember: T.GroupMember } + export interface HostConnected extends Interface { + type: "hostConnected" + protocol: string + transportHost: string + } + + export interface HostDisconnected extends Interface { + type: "hostDisconnected" + protocol: string + transportHost: string + } + + export interface SubscriptionStatus extends Interface { + type: "subscriptionStatus" + server: string + subscriptionStatus: T.SubscriptionStatus + connections: string[] + } + export interface MessageError extends Interface { type: "messageError" user: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index ea49478b15..684aeec7af 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -10,6 +10,9 @@ export type ChatResponse = | CR.ChatItemReaction | CR.ChatItemUpdated | CR.ChatItemsDeleted + | CR.ChatRunning + | CR.ChatStarted + | CR.ChatStopped | CR.CmdOk | CR.ChatCmdError | CR.ConnectionPlan @@ -58,6 +61,9 @@ export namespace CR { | "chatItemReaction" | "chatItemUpdated" | "chatItemsDeleted" + | "chatRunning" + | "chatStarted" + | "chatStopped" | "cmdOk" | "chatCmdError" | "connectionPlan" @@ -140,6 +146,18 @@ export namespace CR { timed: boolean } + export interface ChatRunning extends Interface { + type: "chatRunning" + } + + export interface ChatStarted extends Interface { + type: "chatStarted" + } + + export interface ChatStopped extends Interface { + type: "chatStopped" + } + export interface CmdOk extends Interface { type: "cmdOk" user_?: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 7ab77b8705..7075d4b7ca 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -778,6 +778,7 @@ export interface CIMeta { itemTimed?: CITimed itemLive?: boolean userMention: boolean + hasLink: boolean deletable: boolean editable: boolean forwardedByMember?: number // int64 @@ -1546,7 +1547,7 @@ export interface ChatRef { export namespace ChatRef { export function cmdString(self: ChatRef): string { - return self.chatType.toString() + self.chatId + (self.chatScope ? self.chatScope.toString() : '') + return ChatType.cmdString(self.chatType) + self.chatId + (self.chatScope ? GroupChatScope.cmdString(self.chatScope) : '') } } @@ -1688,15 +1689,69 @@ export interface ComposedMessage { mentions: {[key: string]: number} // string : int64 } -export enum ConnStatus { - New = "new", - Prepared = "prepared", - Joined = "joined", - Requested = "requested", - Accepted = "accepted", - Snd_ready = "snd-ready", - Ready = "ready", - Deleted = "deleted", +export type ConnStatus = + | ConnStatus.New + | ConnStatus.Prepared + | ConnStatus.Joined + | ConnStatus.Requested + | ConnStatus.Accepted + | ConnStatus.SndReady + | ConnStatus.Ready + | ConnStatus.Deleted + | ConnStatus.Failed + +export namespace ConnStatus { + export type Tag = + | "new" + | "prepared" + | "joined" + | "requested" + | "accepted" + | "sndReady" + | "ready" + | "deleted" + | "failed" + + interface Interface { + type: Tag + } + + export interface New extends Interface { + type: "new" + } + + export interface Prepared extends Interface { + type: "prepared" + } + + export interface Joined extends Interface { + type: "joined" + } + + export interface Requested extends Interface { + type: "requested" + } + + export interface Accepted extends Interface { + type: "accepted" + } + + export interface SndReady extends Interface { + type: "sndReady" + } + + export interface Ready extends Interface { + type: "ready" + } + + export interface Deleted extends Interface { + type: "deleted" + } + + export interface Failed extends Interface { + type: "failed" + connError: string + } } export enum ConnType { @@ -2220,6 +2275,7 @@ export type Format = | Format.StrikeThrough | Format.Snippet | Format.Secret + | Format.Small | Format.Colored | Format.Uri | Format.HyperLink @@ -2236,6 +2292,7 @@ export namespace Format { | "strikeThrough" | "snippet" | "secret" + | "small" | "colored" | "uri" | "hyperLink" @@ -2269,6 +2326,10 @@ export namespace Format { type: "secret" } + export interface Small extends Interface { + type: "small" + } + export interface Colored extends Interface { type: "colored" color: Color @@ -4294,6 +4355,37 @@ export namespace StoreError { } } +export type SubscriptionStatus = + | SubscriptionStatus.Active + | SubscriptionStatus.Pending + | SubscriptionStatus.Removed + | SubscriptionStatus.NoSub + +export namespace SubscriptionStatus { + export type Tag = "active" | "pending" | "removed" | "noSub" + + interface Interface { + type: Tag + } + + export interface Active extends Interface { + type: "active" + } + + export interface Pending extends Interface { + type: "pending" + } + + export interface Removed extends Interface { + type: "removed" + subError: string + } + + export interface NoSub extends Interface { + type: "noSub" + } +} + export enum SwitchPhase { Started = "started", Confirmed = "confirmed", diff --git a/packages/simplex-chat-client/typescript/README.md b/packages/simplex-chat-client/typescript/README.md index c1756dc82c..a67e822de2 100644 --- a/packages/simplex-chat-client/typescript/README.md +++ b/packages/simplex-chat-client/typescript/README.md @@ -1,4 +1,8 @@ -# SimpleX Chat JavaScript client +# SimpleX Chat JavaScript WebRTC client + +**THIS PACKAGE IS DEPRECATED** + +Use [SimpleX Chat Node.js library](https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-nodejs#readme) instead of this package. This is a TypeScript library that defines WebSocket API client for [SimpleX Chat terminal CLI](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/CLI.md) that should be run as a WebSockets server on any port: @@ -24,7 +28,7 @@ Please share your use cases and implementations. ## Quick start ``` -npm i simplex-chat +npm i @simplex-chat/webrtc-client@6.5.0-beta.3 npm run build ``` diff --git a/packages/simplex-chat-client/typescript/package.json b/packages/simplex-chat-client/typescript/package.json index b721dbcce2..c9d6165336 100644 --- a/packages/simplex-chat-client/typescript/package.json +++ b/packages/simplex-chat-client/typescript/package.json @@ -1,6 +1,6 @@ { - "name": "simplex-chat", - "version": "0.3.0", + "name": "@simplex-chat/webrtc-client", + "version": "6.5.0-beta.3", "description": "SimpleX Chat client", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -38,12 +38,12 @@ "bugs": { "url": "https://github.com/simplex-chat/simplex-chat/issues" }, - "homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-client/typescript#readme", + "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript#readme", "dependencies": { + "@simplex-chat/types": "^0.3.0", "isomorphic-ws": "^4.0.1" }, "devDependencies": { - "@simplex-chat/types": "^0.1.0", "@types/jest": "^27.5.1", "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.23.0", diff --git a/packages/simplex-chat-client/typescript/tests/client.test.ts b/packages/simplex-chat-client/typescript/tests/client.test.ts index 9cd0a365f1..1c5c4d4baa 100644 --- a/packages/simplex-chat-client/typescript/tests/client.test.ts +++ b/packages/simplex-chat-client/typescript/tests/client.test.ts @@ -24,7 +24,7 @@ describe.skip("ChatClient (expects SimpleX Chat server with a user, without cont const r2 = await c.msgQ.dequeue() assert.strictEqual(r1.type, "contactConnecting") assert.strictEqual(r2.type, "contactConnected") - const contact1 = (r1 as CEvt.ContactConnected).contact + const contact1 = (r1 as CEvt.ContactConnecting).contact // const contact2 = (r2 as C.CRContactConnected).contact const r3 = await c.apiSendTextMessage(T.ChatType.Direct, contact1.contactId, "hello") assert(r3[0].chatItem.content.type === "sndMsgContent" && r3[0].chatItem.content.msgContent.text === "hello") diff --git a/packages/simplex-chat-nodejs/.gitignore b/packages/simplex-chat-nodejs/.gitignore new file mode 100644 index 0000000000..322e38bfda --- /dev/null +++ b/packages/simplex-chat-nodejs/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +package-lock.json +.vscode +build/ +libs/ +dist/ +coverage/ +tmp/ diff --git a/packages/simplex-chat-nodejs/.npmignore b/packages/simplex-chat-nodejs/.npmignore new file mode 100644 index 0000000000..26fdd85dff --- /dev/null +++ b/packages/simplex-chat-nodejs/.npmignore @@ -0,0 +1,3 @@ +libs/ +build/ +node_modules/ diff --git a/packages/simplex-chat-nodejs/LICENSE b/packages/simplex-chat-nodejs/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/packages/simplex-chat-nodejs/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/simplex-chat-nodejs/README.md b/packages/simplex-chat-nodejs/README.md new file mode 100644 index 0000000000..e75dab3f96 --- /dev/null +++ b/packages/simplex-chat-nodejs/README.md @@ -0,0 +1,92 @@ +# SimpleX Chat Node.js library + +This library replaced now deprecated [SimpleX Chat WebRTC TypeScript client](https://www.npmjs.com/package/@simplex-chat/webrtc-client). + +## Use cases + +- chat bots: you can implement any logic of connecting with and communicating with SimpleX Chat users. Using chat groups a chat bot can connect SimpleX Chat users with each other. +- control of the equipment: e.g. servers or home automation. SimpleX Chat provides secure and authorised connections, so this is more secure than using rest APIs. +- any scenarios of scripted message sending. +- chat and chat-based interfaces. + +Please share your use cases and implementations. + +## Quick start: a simple bot + +``` +npm i simplex-chat@6.5.0-beta.4.4 +``` + +Simple bot that replies with squares of numbers you send to it: + +```javascript +(async () => { + const {bot} = await import("simplex-chat") + // if you are running from this GitHub repo: + // const {bot} = await import("../dist/index.js") + const [chat, _user, _address] = await bot.run({ + profile: {displayName: "Squaring bot example", fullName: ""}, + dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + options: { + addressSettings: {welcomeMessage: "Send a number, I will square it.", + }, + onMessage: async (ci, content) => { + const n = +content.text + const reply = typeof n === "number" && !isNaN(n) + ? `${n} * ${n} = ${n * n}` + : `this is not a number` + await chat.apiSendTextReply(ci, reply) + } + }) +})() +``` + +If you installed this package as dependency, you can run this example with: + +```sh +node ./node_modules/simplex-chat/examples/squaring-bot-readme.js +``` + +If you run it on Mac, the first time it will take 20-30 seconds for MacOS to verify the library. + +If you cloned this repository, you can: + +``` +cd ./packages/simplex-chat-nodejs +npm install +npm run build +node ./examples/squaring-bot-readme.js +``` + +There is an example with more options in [./examples/squaring-bot.ts](./examples/squaring-bot.ts). + +You can run it with: `npx ts-node ./examples/squaring-bot.ts` + +## Documentation + +The library docs are [here](./docs/README.md). + +Library provides these modules: +- [bot](./docs/Namespace.bot.md): a simple declarative API to run a chat-bot with a single function call. It automates creating and updating of the bot profile, address and bot commands shown in the app UI. +- [api](./docs/Namespace.api.md): an API to send chat commands and receive chat events to/from chat core. You need to use it in bot event handlers, and for any other use cases. +- [core](./docs/Namespace.core.md): a low level API to the core library - the same that is used in desktop clients. You are unlikely to ever need to use this module directly. +- [util](./docs/Namespace.util.md): useful functions for chat events and types. + + +This library uses [@simplex-chat/types](https://www.npmjs.com/package/@simplex-chat/types) package with auto-generated [bot API types](../../bots/api/README.md). + +## Supported chat functions + +Library provides types and functions to: + +- create and change user profile (although, in most cases you can do it manually, via SimpleX Chat terminal app). +- create and accept invitations or connect with the contacts. +- create and manage long-term user address, accepting connection requests automatically. +- send, receive, delete and update messages, and add message reactions. +- create, join and manage group. +- send and receive files. +- etc. + +## License + +[AGPL v3](./LICENSE) diff --git a/packages/simplex-chat-nodejs/binding.gyp b/packages/simplex-chat-nodejs/binding.gyp new file mode 100644 index 0000000000..09c63cecba --- /dev/null +++ b/packages/simplex-chat-nodejs/binding.gyp @@ -0,0 +1,50 @@ +{ + "targets": [ + { + "target_name": "simplex", + "sources": [ "cpp/simplex.cc" ], + "include_dirs": [ + " +#include +#include +#include +#include +#include "simplex.h" + +namespace simplex { + +using namespace Napi; + +void haskell_init() { +#ifdef _WIN32 + // non-moving GC is broken on windows with GHC 9.4-9.6.3 + int argc = 5; + const char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A64m", // chunk size for new allocations + "-H64m", // initial heap size + "--install-signal-handlers=no", + nullptr}; +#else + int argc = 6; + const char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A64m", // chunk size for new allocations + "-H64m", // initial heap size + "-xn", // non-moving GC + "--install-signal-handlers=no", + nullptr}; +#endif + char **pargv = const_cast(argv); + hs_init_with_rtsopts(&argc, &pargv); +} + +class ResultAsyncWorker : public AsyncWorker { + public: + using ExecuteFn = std::function; + using ResultProcessor = std::function; + + ResultAsyncWorker(Function& callback, ExecuteFn execute_fn, ResultProcessor result_processor = nullptr) + : AsyncWorker(callback), execute_fn_(std::move(execute_fn)), result_processor_(std::move(result_processor)) {} + + void Execute() override { + execute_fn_(this); + } + + void OnOK() override { + HandleScope scope(Env()); + if (result_processor_) { + result_processor_(this, Env()); + } else { + Callback().Call({Env().Null(), String::New(Env(), result_)}); + } + } + + void OnError(const Error& e) override { + HandleScope scope(Env()); + Callback().Call({e.Value(), Env().Undefined()}); + } + + void SetResult(std::string result) { + result_ = std::move(result); + } + + void SetWorkerError(const std::string& msg) { + SetError(msg); + } + + const std::string& GetStringResult() const { + return result_; + } + + void SetCtrl(uintptr_t ctrl) { + ctrl_ = ctrl; + } + + uintptr_t GetCtrl() const { + return ctrl_; + } + + protected: + std::string result_; + uintptr_t ctrl_ = 0; + + private: + ExecuteFn execute_fn_; + ResultProcessor result_processor_; +}; + +class BinaryAsyncWorker : public AsyncWorker { + public: + using ExecuteFn = std::function; + + BinaryAsyncWorker(Function& callback, ExecuteFn execute_fn) + : AsyncWorker(callback), execute_fn_(std::move(execute_fn)) {} + + void Execute() override { + execute_fn_(this); + } + + void OnOK() override { + HandleScope scope(Env()); + if (original_buf == nullptr || binary_len == 0) { + Callback().Call({Env().Null(), Env().Undefined()}); + return; + } + char* data_ptr = original_buf + 5; + auto finalizer = [](Napi::Env env, char* finalize_data, char* orig) { + free(orig); + }; + Napi::Buffer buffer = Napi::Buffer::New(Env(), data_ptr, binary_len, finalizer, original_buf); + Callback().Call({Env().Null(), buffer}); + } + + void OnError(const Error& e) override { + HandleScope scope(Env()); + Callback().Call({e.Value(), Env().Undefined()}); + } + + void SetWorkerError(const std::string& msg) { + SetError(msg); + } + + char* original_buf = nullptr; + size_t binary_len = 0; + + private: + ExecuteFn execute_fn_; +}; + +// Helper for converting chat_ctrl pointer to BigInt +Napi::BigInt ToChatCtrlBigInt(Napi::Env env, uintptr_t ctrl) { + return Napi::BigInt::New(env, static_cast(ctrl)); +} + +// Helper for converting BigInt to chat_ctrl pointer +chat_ctrl FromChatCtrlBigInt(const Napi::Value& value) { + Napi::Env env = value.Env(); + if (!value.IsBigInt()) { + Napi::TypeError::New(env, "Expected BigInt for ctrl").ThrowAsJavaScriptException(); + return nullptr; + } + Napi::BigInt big = value.As(); + bool lossless; + uint64_t val = big.Uint64Value(&lossless); + if (!lossless) { + Napi::TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException(); + return nullptr; + } + return reinterpret_cast(val); +} + +// Helper for handling common C result patterns (no empty check) +void HandleCResult(ResultAsyncWorker* worker, char* c_res, const std::string& func_name) { + if (c_res == nullptr) { + worker->SetWorkerError(func_name + " failed"); + return; + } + std::string res = c_res; + free(c_res); + worker->SetResult(res); +} + +Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) { + Promise::Deferred deferred = Promise::Deferred::New(env); + cb_out = Function::New(env, [deferred](const CallbackInfo& args) { + if (!args[0].IsNull() && !args[0].IsUndefined()) { + deferred.Reject(args[0]); + } else { + deferred.Resolve(args[1]); + } + }); + return deferred.Promise(); +} + +// Common result processors +ResultAsyncWorker::ResultProcessor MigrateResultProcessor() { + return [](ResultAsyncWorker* worker, Napi::Env env) { + Napi::Array arr = Napi::Array::New(env, 2); + arr.Set(0u, ToChatCtrlBigInt(env, worker->GetCtrl())); + arr.Set(1u, Napi::String::New(env, worker->GetStringResult())); + worker->Callback().Call({env.Null(), arr}); + }; +} + +// Refactored functions using common patterns + +Value ChatMigrateInit(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected three string arguments").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + std::string path = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string confirm = args[2].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [path, key, confirm](ResultAsyncWorker* worker) { + chat_ctrl ctrl = nullptr; + char* c_res = chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl); + worker->SetCtrl(reinterpret_cast(ctrl)); + HandleCResult(worker, c_res, "chat_migrate_init"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor()); + worker->Queue(); + + return promise; +} + +Value ChatCloseStore(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 1 || !args[0].IsBigInt()) { + TypeError::New(env, "Expected bigint (ctrl)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl](ResultAsyncWorker* worker) { + char* c_res = chat_close_store(ctrl); + HandleCResult(worker, c_res, "chat_close_store"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatSendCmd(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsString()) { + TypeError::New(env, "Expected bigint (ctrl) and string (cmd)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string cmd = args[1].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, cmd](ResultAsyncWorker* worker) { + char* c_res = chat_send_cmd(ctrl, cmd.c_str()); + HandleCResult(worker, c_res, "chat_send_cmd"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatRecvMsgWait(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsNumber()) { + TypeError::New(env, "Expected bigint (ctrl), number (wait)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + int wait = static_cast(args[1].As().Int32Value()); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, wait](ResultAsyncWorker* worker) { + char* c_res = chat_recv_msg_wait(ctrl, wait); + HandleCResult(worker, c_res, "chat_recv_msg_wait"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatWriteFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsArrayBuffer()) { + TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string path = args[1].As().Utf8Value(); + ArrayBuffer ab = args[2].As(); + char* data = static_cast(ab.Data()); + size_t len = ab.ByteLength(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, path, ab, data, len](ResultAsyncWorker* worker) { + (void)ab; // to keep ArrayBuffer alive + char* c_res = chat_write_file(ctrl, path.c_str(), data, static_cast(len)); + HandleCResult(worker, c_res, "chat_write_file"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatReadFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected three strings (path, key, nonce)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + std::string path = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string nonce = args[2].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [path, key, nonce](BinaryAsyncWorker* worker) { + char* buf = chat_read_file(path.c_str(), key.c_str(), nonce.c_str()); + if (buf == nullptr) { + worker->SetWorkerError("chat_read_file failed"); + return; + } + char status = buf[0]; + if (status == 1) { + std::string err = buf + 1; + free(buf); + worker->SetWorkerError(err); + return; + } else if (status == 0) { + uint32_t len = *(uint32_t*)(buf + 1); + worker->original_buf = buf; + worker->binary_len = len; + } else { + free(buf); + worker->SetWorkerError("Unexpected status from chat_read_file"); + return; + } + }; + + BinaryAsyncWorker* worker = new BinaryAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatEncryptFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected bigint (ctrl), two strings (fromPath, toPath)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string fromPath = args[1].As().Utf8Value(); + std::string toPath = args[2].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, fromPath, toPath](ResultAsyncWorker* worker) { + char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()); + HandleCResult(worker, c_res, "chat_encrypt_file"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatDecryptFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsString()) { + TypeError::New(env, "Expected four strings (fromPath, key, nonce, toPath)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + std::string fromPath = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string nonce = args[2].As().Utf8Value(); + std::string toPath = args[3].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [fromPath, key, nonce, toPath](ResultAsyncWorker* worker) { + char* c_res = chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str()); + HandleCResult(worker, c_res, "chat_decrypt_file"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Object Init(Env env, Object exports) { + haskell_init(); + exports.Set("chat_migrate_init", Function::New(env, ChatMigrateInit)); + exports.Set("chat_close_store", Function::New(env, ChatCloseStore)); + exports.Set("chat_send_cmd", Function::New(env, ChatSendCmd)); + exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait)); + exports.Set("chat_write_file", Function::New(env, ChatWriteFile)); + exports.Set("chat_read_file", Function::New(env, ChatReadFile)); + exports.Set("chat_encrypt_file", Function::New(env, ChatEncryptFile)); + exports.Set("chat_decrypt_file", Function::New(env, ChatDecryptFile)); + return exports; +} + +NODE_API_MODULE(simplex, Init) + +} diff --git a/packages/simplex-chat-nodejs/cpp/simplex.h b/packages/simplex-chat-nodejs/cpp/simplex.h new file mode 100644 index 0000000000..8e579626ed --- /dev/null +++ b/packages/simplex-chat-nodejs/cpp/simplex.h @@ -0,0 +1,46 @@ +// +// simplex.h +// SimpleX +// +// Created by Evgeny on 30/05/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +#ifndef SimpleX_h +#define SimpleX_h + +extern "C" void hs_init(int argc, char **argv[]); +extern "C" void hs_init_with_rtsopts(int * argc, char **argv[]); + +typedef long* chat_ctrl; + +// the last parameter is used to return the pointer to chat controller +extern "C" char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); +extern "C" char *chat_close_store(chat_ctrl ctrl); +extern "C" char *chat_reopen_store(chat_ctrl ctrl); +extern "C" char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); +extern "C" char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); +extern "C" char *chat_parse_markdown(const char *str); +extern "C" char *chat_parse_server(const char *str); +extern "C" char *chat_password_hash(const char *pwd, const char *salt); +extern "C" char *chat_valid_name(const char *name); +extern "C" int chat_json_length(const char *str); +extern "C" char *chat_encrypt_media(chat_ctrl ctrl, const char *key, const char *frame, const int len); +extern "C" char *chat_decrypt_media(const char *key, const char *frame, const int len); + +// chat_write_file returns null-terminated string with JSON of WriteFileResult +extern "C" char *chat_write_file(chat_ctrl ctrl, const char *path, const char *data, const int len); + +// chat_read_file returns a buffer with: +// result status (1 byte), then if +// status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length. +// status == 1 (error): null-terminated error message string. +extern "C" char *chat_read_file(const char *path, const char *key, const char *nonce); + +// chat_encrypt_file returns null-terminated string with JSON of WriteFileResult +extern "C" char *chat_encrypt_file(chat_ctrl ctrl, const char *fromPath, const char *toPath); + +// chat_decrypt_file returns null-terminated string with the error message +extern "C" char *chat_decrypt_file(const char *fromPath, const char *key, const char *nonce, const char *toPath); + +#endif /* simplex_h */ \ No newline at end of file diff --git a/packages/simplex-chat-nodejs/docs/Namespace.api.md b/packages/simplex-chat-nodejs/docs/Namespace.api.md new file mode 100644 index 0000000000..a1b3d2ca5a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/Namespace.api.md @@ -0,0 +1,32 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / api + +# api + +An API to send chat commands and receive chat events to/from chat core. +You need to use it in bot event handlers, and for any other use cases. + +## Enumerations + +- [ConnReqType](api.Enumeration.ConnReqType.md) + +## Classes + +- [ChatApi](api.Class.ChatApi.md) +- [ChatCommandError](api.Class.ChatCommandError.md) + +## Interfaces + +- [BotAddressSettings](api.Interface.BotAddressSettings.md) + +## Type Aliases + +- [EventSubscriberFunc](api.TypeAlias.EventSubscriberFunc.md) +- [EventSubscribers](api.TypeAlias.EventSubscribers.md) + +## Variables + +- [defaultBotAddressSettings](api.Variable.defaultBotAddressSettings.md) diff --git a/packages/simplex-chat-nodejs/docs/Namespace.bot.md b/packages/simplex-chat-nodejs/docs/Namespace.bot.md new file mode 100644 index 0000000000..447b0b6f68 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/Namespace.bot.md @@ -0,0 +1,20 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / bot + +# bot + +A simple declarative API to run a chat-bot with a single function call. +It automates creating and updating of the bot profile, address and bot commands shown in the app UI. + +## Interfaces + +- [BotConfig](bot.Interface.BotConfig.md) +- [BotDbOpts](bot.Interface.BotDbOpts.md) +- [BotOptions](bot.Interface.BotOptions.md) + +## Functions + +- [run](bot.Function.run.md) diff --git a/packages/simplex-chat-nodejs/docs/Namespace.core.md b/packages/simplex-chat-nodejs/docs/Namespace.core.md new file mode 100644 index 0000000000..82b0d9ffba --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/Namespace.core.md @@ -0,0 +1,48 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / core + +# core + +A low level API to the core library - the same that is used in desktop clients. +You are unlikely to ever need to use this module directly. + +## Namespaces + +- [DBMigrationError](core.Namespace.DBMigrationError.md) +- [MigrationError](core.Namespace.MigrationError.md) +- [MTRError](core.Namespace.MTRError.md) + +## Enumerations + +- [MigrationConfirmation](core.Enumeration.MigrationConfirmation.md) + +## Classes + +- [ChatAPIError](core.Class.ChatAPIError.md) +- [ChatInitError](core.Class.ChatInitError.md) + +## Interfaces + +- [APIResult](core.Interface.APIResult.md) +- [CryptoArgs](core.Interface.CryptoArgs.md) +- [UpMigration](core.Interface.UpMigration.md) + +## Type Aliases + +- [DBMigrationError](core.TypeAlias.DBMigrationError.md) +- [MigrationError](core.TypeAlias.MigrationError.md) +- [MTRError](core.TypeAlias.MTRError.md) + +## Functions + +- [chatCloseStore](core.Function.chatCloseStore.md) +- [chatDecryptFile](core.Function.chatDecryptFile.md) +- [chatEncryptFile](core.Function.chatEncryptFile.md) +- [chatMigrateInit](core.Function.chatMigrateInit.md) +- [chatReadFile](core.Function.chatReadFile.md) +- [chatRecvMsgWait](core.Function.chatRecvMsgWait.md) +- [chatSendCmd](core.Function.chatSendCmd.md) +- [chatWriteFile](core.Function.chatWriteFile.md) diff --git a/packages/simplex-chat-nodejs/docs/Namespace.util.md b/packages/simplex-chat-nodejs/docs/Namespace.util.md new file mode 100644 index 0000000000..a6e7d9c7f3 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/Namespace.util.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / util + +# util + +Useful functions for chat events and types. + +## Interfaces + +- [BotCommand](util.Interface.BotCommand.md) + +## Functions + +- [botAddressSettings](util.Function.botAddressSettings.md) +- [chatInfoName](util.Function.chatInfoName.md) +- [chatInfoRef](util.Function.chatInfoRef.md) +- [ciBotCommand](util.Function.ciBotCommand.md) +- [ciContentText](util.Function.ciContentText.md) +- [contactAddressStr](util.Function.contactAddressStr.md) +- [fromLocalProfile](util.Function.fromLocalProfile.md) +- [reactionText](util.Function.reactionText.md) +- [senderName](util.Function.senderName.md) diff --git a/packages/simplex-chat-nodejs/docs/README.md b/packages/simplex-chat-nodejs/docs/README.md new file mode 100644 index 0000000000..f954730ed6 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/README.md @@ -0,0 +1,12 @@ +**simplex-chat** + +*** + +# simplex-chat + +## Namespaces + +- [api](Namespace.api.md) +- [bot](Namespace.bot.md) +- [core](Namespace.core.md) +- [util](Namespace.util.md) diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md new file mode 100644 index 0000000000..6812740aa2 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md @@ -0,0 +1,1602 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / ChatApi + +# Class: ChatApi + +Defined in: [src/api.ts:62](../src/api.ts#L62) + +Main API class for interacting with the chat core library. + +## Properties + +### ctrl\_ + +> `protected` **ctrl\_**: `bigint` \| `undefined` + +Defined in: [src/api.ts:68](../src/api.ts#L68) + +## Accessors + +### ctrl + +#### Get Signature + +> **get** **ctrl**(): `bigint` + +Defined in: [src/api.ts:295](../src/api.ts#L295) + +Chat controller reference + +##### Returns + +`bigint` + +*** + +### initialized + +#### Get Signature + +> **get** **initialized**(): `boolean` + +Defined in: [src/api.ts:281](../src/api.ts#L281) + +Chat controller is initialized + +##### Returns + +`boolean` + +*** + +### started + +#### Get Signature + +> **get** **started**(): `boolean` + +Defined in: [src/api.ts:288](../src/api.ts#L288) + +Chat controller is started + +##### Returns + +`boolean` + +## Methods + +### apiAcceptContactRequest() + +> **apiAcceptContactRequest**(`contactReqId`): `Promise`\<`Contact`\> + +Defined in: [src/api.ts:697](../src/api.ts#L697) + +Accept contact request. +Network usage: interactive. + +#### Parameters + +##### contactReqId + +`number` + +#### Returns + +`Promise`\<`Contact`\> + +*** + +### apiAcceptMember() + +> **apiAcceptMember**(`groupId`, `groupMemberId`, `memberRole`): `Promise`\<`GroupMember`\> + +Defined in: [src/api.ts:517](../src/api.ts#L517) + +Accept group member. Requires Admin role. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### groupMemberId + +`number` + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`GroupMember`\> + +*** + +### apiAddMember() + +> **apiAddMember**(`groupId`, `contactId`, `memberRole`): `Promise`\<`GroupMember`\> + +Defined in: [src/api.ts:497](../src/api.ts#L497) + +Add contact to group. Requires bot to have Admin role. +Network usage: interactive. + +#### Parameters + +##### groupId + +`number` + +##### contactId + +`number` + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`GroupMember`\> + +*** + +### apiBlockMembersForAll() + +> **apiBlockMembersForAll**(`groupId`, `groupMemberIds`, `blocked`): `Promise`\<`void`\> + +Defined in: [src/api.ts:537](../src/api.ts#L537) + +Block members. Requires Moderator role. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### groupMemberIds + +`number`[] + +##### blocked + +`boolean` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiCancelFile() + +> **apiCancelFile**(`fileId`): `Promise`\<`void`\> + +Defined in: [src/api.ts:487](../src/api.ts#L487) + +Cancel file. +Network usage: background. + +#### Parameters + +##### fileId + +`number` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiChatItemReaction() + +> **apiChatItemReaction**(`chatType`, `chatId`, `chatItemId`, `add`, `reaction`): `Promise`\<`ChatItemDeletion`[]\> + +Defined in: [src/api.ts:461](../src/api.ts#L461) + +Add/remove message reaction. +Network usage: background. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### chatItemId + +`number` + +##### add + +`boolean` + +##### reaction + +`MsgReaction` + +#### Returns + +`Promise`\<`ChatItemDeletion`[]\> + +*** + +### apiConnect() + +> **apiConnect**(`userId`, `incognito`, `preparedLink?`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> + +Defined in: [src/api.ts:666](../src/api.ts#L666) + +Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +##### incognito + +`boolean` + +##### preparedLink? + +`CreatedConnLink` + +#### Returns + +`Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> + +*** + +### apiConnectActiveUser() + +> **apiConnectActiveUser**(`connLink`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> + +Defined in: [src/api.ts:675](../src/api.ts#L675) + +Connect via SimpleX link as string in the active user profile. +Network usage: interactive. + +#### Parameters + +##### connLink + +`string` + +#### Returns + +`Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> + +*** + +### apiConnectPlan() + +> **apiConnectPlan**(`userId`, `connectionLink`): `Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\> + +Defined in: [src/api.ts:656](../src/api.ts#L656) + +Determine SimpleX link type and if the bot is already connected via this link. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +##### connectionLink + +`string` + +#### Returns + +`Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\> + +*** + +### apiCreateActiveUser() + +> **apiCreateActiveUser**(`profile?`): `Promise`\<`User`\> + +Defined in: [src/api.ts:774](../src/api.ts#L774) + +Create new user profile +Network usage: no. + +#### Parameters + +##### profile? + +`Profile` + +#### Returns + +`Promise`\<`User`\> + +*** + +### apiCreateGroupLink() + +> **apiCreateGroupLink**(`groupId`, `memberRole`): `Promise`\<`string`\> + +Defined in: [src/api.ts:597](../src/api.ts#L597) + +Create group link. +Network usage: interactive. + +#### Parameters + +##### groupId + +`number` + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`string`\> + +*** + +### apiCreateLink() + +> **apiCreateLink**(`userId`): `Promise`\<`string`\> + +Defined in: [src/api.ts:643](../src/api.ts#L643) + +Create 1-time invitation link. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`string`\> + +*** + +### apiCreateUserAddress() + +> **apiCreateUserAddress**(`userId`): `Promise`\<`CreatedConnLink`\> + +Defined in: [src/api.ts:312](../src/api.ts#L312) + +Create bot address. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`CreatedConnLink`\> + +*** + +### apiDeleteChat() + +> **apiDeleteChat**(`chatType`, `chatId`, `deleteMode`): `Promise`\<`void`\> + +Defined in: [src/api.ts:737](../src/api.ts#L737) + +Delete chat. +Network usage: background. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### deleteMode + +`ChatDeleteMode` = `...` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiDeleteChatItems() + +> **apiDeleteChatItems**(`chatType`, `chatId`, `chatItemIds`, `deleteMode`): `Promise`\<`ChatItemDeletion`[]\> + +Defined in: [src/api.ts:436](../src/api.ts#L436) + +Delete message. +Network usage: background. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### chatItemIds + +`number`[] + +##### deleteMode + +`CIDeleteMode` + +#### Returns + +`Promise`\<`ChatItemDeletion`[]\> + +*** + +### apiDeleteGroupLink() + +> **apiDeleteGroupLink**(`groupId`): `Promise`\<`void`\> + +Defined in: [src/api.ts:619](../src/api.ts#L619) + +Delete group link. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiDeleteMemberChatItem() + +> **apiDeleteMemberChatItem**(`groupId`, `chatItemIds`): `Promise`\<`ChatItemDeletion`[]\> + +Defined in: [src/api.ts:451](../src/api.ts#L451) + +Moderate message. Requires Moderator role (and higher than message author's). +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### chatItemIds + +`number`[] + +#### Returns + +`Promise`\<`ChatItemDeletion`[]\> + +*** + +### apiDeleteUser() + +> **apiDeleteUser**(`userId`, `delSMPQueues`, `viewPwd?`): `Promise`\<`void`\> + +Defined in: [src/api.ts:804](../src/api.ts#L804) + +Delete user profile. +Network usage: background. + +#### Parameters + +##### userId + +`number` + +##### delSMPQueues + +`boolean` + +##### viewPwd? + +`string` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiDeleteUserAddress() + +> **apiDeleteUserAddress**(`userId`): `Promise`\<`void`\> + +Defined in: [src/api.ts:322](../src/api.ts#L322) + +Deletes a user address. +Network usage: background. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiGetActiveUser() + +> **apiGetActiveUser**(): `Promise`\<`User` \| `undefined`\> + +Defined in: [src/api.ts:754](../src/api.ts#L754) + +Get active user profile +Network usage: no. + +#### Returns + +`Promise`\<`User` \| `undefined`\> + +*** + +### apiGetGroupLink() + +> **apiGetGroupLink**(`groupId`): `Promise`\<`GroupLink`\> + +Defined in: [src/api.ts:628](../src/api.ts#L628) + +Get group link. +Network usage: no. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`GroupLink`\> + +*** + +### apiGetGroupLinkStr() + +> **apiGetGroupLinkStr**(`groupId`): `Promise`\<`string`\> + +Defined in: [src/api.ts:634](../src/api.ts#L634) + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`string`\> + +*** + +### apiGetUserAddress() + +> **apiGetUserAddress**(`userId`): `Promise`\<`UserContactLink` \| `undefined`\> + +Defined in: [src/api.ts:332](../src/api.ts#L332) + +Get bot address and settings. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`UserContactLink` \| `undefined`\> + +*** + +### apiJoinGroup() + +> **apiJoinGroup**(`groupId`): `Promise`\<`GroupInfo`\> + +Defined in: [src/api.ts:507](../src/api.ts#L507) + +Join group. +Network usage: interactive. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`GroupInfo`\> + +*** + +### apiLeaveGroup() + +> **apiLeaveGroup**(`groupId`): `Promise`\<`GroupInfo`\> + +Defined in: [src/api.ts:557](../src/api.ts#L557) + +Leave group. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`GroupInfo`\> + +*** + +### apiListContacts() + +> **apiListContacts**(`userId`): `Promise`\<`Contact`[]\> + +Defined in: [src/api.ts:717](../src/api.ts#L717) + +Get contacts. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`Contact`[]\> + +*** + +### apiListGroups() + +> **apiListGroups**(`userId`, `contactId?`, `search?`): `Promise`\<`GroupInfo`[]\> + +Defined in: [src/api.ts:727](../src/api.ts#L727) + +Get groups. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +##### contactId? + +`number` + +##### search? + +`string` + +#### Returns + +`Promise`\<`GroupInfo`[]\> + +*** + +### apiListMembers() + +> **apiListMembers**(`groupId`): `Promise`\<`GroupMember`[]\> + +Defined in: [src/api.ts:567](../src/api.ts#L567) + +Get group members. +Network usage: no. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`GroupMember`[]\> + +*** + +### apiListUsers() + +> **apiListUsers**(): `Promise`\<`UserInfo`[]\> + +Defined in: [src/api.ts:784](../src/api.ts#L784) + +Get all user profiles +Network usage: no. + +#### Returns + +`Promise`\<`UserInfo`[]\> + +*** + +### apiNewGroup() + +> **apiNewGroup**(`userId`, `groupProfile`): `Promise`\<`GroupInfo`\> + +Defined in: [src/api.ts:577](../src/api.ts#L577) + +Create group. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +##### groupProfile + +`GroupProfile` + +#### Returns + +`Promise`\<`GroupInfo`\> + +*** + +### apiReceiveFile() + +> **apiReceiveFile**(`fileId`): `Promise`\<`AChatItem`\> + +Defined in: [src/api.ts:477](../src/api.ts#L477) + +Receive file. +Network usage: no. + +#### Parameters + +##### fileId + +`number` + +#### Returns + +`Promise`\<`AChatItem`\> + +*** + +### apiRejectContactRequest() + +> **apiRejectContactRequest**(`contactReqId`): `Promise`\<`void`\> + +Defined in: [src/api.ts:707](../src/api.ts#L707) + +Reject contact request. The user who sent the request is **not notified**. +Network usage: no. + +#### Parameters + +##### contactReqId + +`number` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiRemoveMembers() + +> **apiRemoveMembers**(`groupId`, `memberIds`, `withMessages`): `Promise`\<`GroupMember`[]\> + +Defined in: [src/api.ts:547](../src/api.ts#L547) + +Remove members. Requires Admin role. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### memberIds + +`number`[] + +##### withMessages + +`boolean` = `false` + +#### Returns + +`Promise`\<`GroupMember`[]\> + +*** + +### apiSendMessages() + +> **apiSendMessages**(`chat`, `messages`, `liveMessage`): `Promise`\<`AChatItem`[]\> + +Defined in: [src/api.ts:381](../src/api.ts#L381) + +Send messages. +Network usage: background. + +#### Parameters + +##### chat + +`ChatInfo` | `ChatRef` | \[`ChatType`, `number`\] + +##### messages + +`ComposedMessage`[] + +##### liveMessage + +`boolean` = `false` + +#### Returns + +`Promise`\<`AChatItem`[]\> + +*** + +### apiSendTextMessage() + +> **apiSendTextMessage**(`chat`, `text`, `inReplyTo?`): `Promise`\<`AChatItem`[]\> + +Defined in: [src/api.ts:403](../src/api.ts#L403) + +Send text message. +Network usage: background. + +#### Parameters + +##### chat + +`ChatInfo` | `ChatRef` | \[`ChatType`, `number`\] + +##### text + +`string` + +##### inReplyTo? + +`number` + +#### Returns + +`Promise`\<`AChatItem`[]\> + +*** + +### apiSendTextReply() + +> **apiSendTextReply**(`chatItem`, `text`): `Promise`\<`AChatItem`[]\> + +Defined in: [src/api.ts:411](../src/api.ts#L411) + +Send text message in reply to received message. +Network usage: background. + +#### Parameters + +##### chatItem + +`AChatItem` + +##### text + +`string` + +#### Returns + +`Promise`\<`AChatItem`[]\> + +*** + +### apiSetActiveUser() + +> **apiSetActiveUser**(`userId`, `viewPwd?`): `Promise`\<`User`\> + +Defined in: [src/api.ts:794](../src/api.ts#L794) + +Set active user profile +Network usage: no. + +#### Parameters + +##### userId + +`number` + +##### viewPwd? + +`string` + +#### Returns + +`Promise`\<`User`\> + +*** + +### apiSetAddressSettings() + +> **apiSetAddressSettings**(`userId`, `__namedParameters`): `Promise`\<`void`\> + +Defined in: [src/api.ts:364](../src/api.ts#L364) + +Set bot address settings. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +##### \_\_namedParameters + +[`BotAddressSettings`](api.Interface.BotAddressSettings.md) + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetContactPrefs() + +> **apiSetContactPrefs**(`contactId`, `preferences`): `Promise`\<`void`\> + +Defined in: [src/api.ts:830](../src/api.ts#L830) + +Configure chat preference overrides for the contact. +Network usage: background. + +#### Parameters + +##### contactId + +`number` + +##### preferences + +`Preferences` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetGroupLinkMemberRole() + +> **apiSetGroupLinkMemberRole**(`groupId`, `memberRole`): `Promise`\<`void`\> + +Defined in: [src/api.ts:610](../src/api.ts#L610) + +Set member role for group link. +Network usage: no. + +#### Parameters + +##### groupId + +`number` + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetMembersRole() + +> **apiSetMembersRole**(`groupId`, `groupMemberIds`, `memberRole`): `Promise`\<`void`\> + +Defined in: [src/api.ts:527](../src/api.ts#L527) + +Set members role. Requires Admin role. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### groupMemberIds + +`number`[] + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetProfileAddress() + +> **apiSetProfileAddress**(`userId`, `enable`): `Promise`\<`UserProfileUpdateSummary`\> + +Defined in: [src/api.ts:350](../src/api.ts#L350) + +Add address to bot profile. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +##### enable + +`boolean` + +#### Returns + +`Promise`\<`UserProfileUpdateSummary`\> + +*** + +### apiUpdateChatItem() + +> **apiUpdateChatItem**(`chatType`, `chatId`, `chatItemId`, `msgContent`, `liveMessage`): `Promise`\<`ChatItem`\> + +Defined in: [src/api.ts:419](../src/api.ts#L419) + +Update message. +Network usage: background. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### chatItemId + +`number` + +##### msgContent + +`MsgContent` + +##### liveMessage + +`false` + +#### Returns + +`Promise`\<`ChatItem`\> + +*** + +### apiUpdateGroupProfile() + +> **apiUpdateGroupProfile**(`groupId`, `groupProfile`): `Promise`\<`GroupInfo`\> + +Defined in: [src/api.ts:587](../src/api.ts#L587) + +Update group profile. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### groupProfile + +`GroupProfile` + +#### Returns + +`Promise`\<`GroupInfo`\> + +*** + +### apiUpdateProfile() + +> **apiUpdateProfile**(`userId`, `profile`): `Promise`\<`UserProfileUpdateSummary` \| `undefined`\> + +Defined in: [src/api.ts:814](../src/api.ts#L814) + +Update user profile. +Network usage: background. + +#### Parameters + +##### userId + +`number` + +##### profile + +`Profile` + +#### Returns + +`Promise`\<`UserProfileUpdateSummary` \| `undefined`\> + +*** + +### close() + +> **close**(): `Promise`\<`void`\> + +Defined in: [src/api.ts:114](../src/api.ts#L114) + +Close chat database. +Usually doesn't need to be called in chat bots. + +#### Returns + +`Promise`\<`void`\> + +*** + +### off() + +> **off**\<`K`\>(`event`, `subscriber`): `void` + +Defined in: [src/api.ts:253](../src/api.ts#L253) + +Unsubscribe all or a specific handler from a specific event. + +#### Type Parameters + +##### K + +`K` *extends* `Tag` + +#### Parameters + +##### event + +`K` + +The event type to unsubscribe from. + +##### subscriber + +An optional subscriber function for the event. + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> | `undefined` + +#### Returns + +`void` + +*** + +### offAny() + +> **offAny**(`receiver`): `void` + +Defined in: [src/api.ts:269](../src/api.ts#L269) + +Unsubscribe all or a specific handler from any events. + +#### Parameters + +##### receiver + +An optional subscriber function for the event. + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`Tag`\> | `undefined` + +#### Returns + +`void` + +*** + +### on() + +#### Call Signature + +> **on**\<`K`\>(`subscribers`): `void` + +Defined in: [src/api.ts:163](../src/api.ts#L163) + +Subscribe multiple event handlers at once. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### subscribers + +[`EventSubscribers`](api.TypeAlias.EventSubscribers.md) + +An object mapping event types (CEvt.Tag) to their subscriber functions. + +##### Returns + +`void` + +##### Throws + +If the same function is subscribed to event. + +#### Call Signature + +> **on**\<`K`\>(`event`, `subscriber`): `void` + +Defined in: [src/api.ts:171](../src/api.ts#L171) + +Subscribe a handler to a specific event. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +The event type to subscribe to. + +###### subscriber + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> + +The subscriber function for the event. + +##### Returns + +`void` + +##### Throws + +If the same function is subscribed to event. + +*** + +### onAny() + +> **onAny**(`receiver`): `void` + +Defined in: [src/api.ts:194](../src/api.ts#L194) + +Subscribe a handler to any event. + +#### Parameters + +##### receiver + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`Tag`\> + +The receiver function for any event. + +#### Returns + +`void` + +#### Throws + +If the same function is subscribed to event. + +*** + +### once() + +> **once**\<`K`\>(`event`, `subscriber`): `void` + +Defined in: [src/api.ts:205](../src/api.ts#L205) + +Subscribe a handler to a specific event to be delivered one time. + +#### Type Parameters + +##### K + +`K` *extends* `Tag` + +#### Parameters + +##### event + +`K` + +The event type to subscribe to. + +##### subscriber + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> + +The subscriber function for the event. + +#### Returns + +`void` + +#### Throws + +If the same function is subscribed to event. + +*** + +### recvChatEvent() + +> **recvChatEvent**(`wait`): `Promise`\<`ChatEvent` \| `undefined`\> + +Defined in: [src/api.ts:304](../src/api.ts#L304) + +#### Parameters + +##### wait + +`number` = `5_000_000` + +#### Returns + +`Promise`\<`ChatEvent` \| `undefined`\> + +*** + +### sendChatCmd() + +> **sendChatCmd**(`cmd`): `Promise`\<`ChatResponse`\> + +Defined in: [src/api.ts:300](../src/api.ts#L300) + +#### Parameters + +##### cmd + +`string` + +#### Returns + +`Promise`\<`ChatResponse`\> + +*** + +### startChat() + +> **startChat**(): `Promise`\<`void`\> + +Defined in: [src/api.ts:88](../src/api.ts#L88) + +Start chat controller. Must be called with the existing user profile. + +#### Returns + +`Promise`\<`void`\> + +*** + +### stopChat() + +> **stopChat**(): `Promise`\<`void`\> + +Defined in: [src/api.ts:102](../src/api.ts#L102) + +Stop chat controller. +Must be called before closing the database. +Usually doesn't need to be called in chat bots. + +#### Returns + +`Promise`\<`void`\> + +*** + +### wait() + +#### Call Signature + +> **wait**\<`K`\>(`event`): `Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> + +Defined in: [src/api.ts:213](../src/api.ts#L213) + +Waits for specific event, with an optional predicate. +Returns `undefined` on timeout if specified. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +##### Returns + +`Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> + +#### Call Signature + +> **wait**\<`K`\>(`event`, `predicate`): `Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> + +Defined in: [src/api.ts:214](../src/api.ts#L214) + +Waits for specific event, with an optional predicate. +Returns `undefined` on timeout if specified. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +###### predicate + +(`event`) => `boolean` | `undefined` + +##### Returns + +`Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> + +#### Call Signature + +> **wait**\<`K`\>(`event`, `timeout`): `Promise`\ + +Defined in: [src/api.ts:215](../src/api.ts#L215) + +Waits for specific event, with an optional predicate. +Returns `undefined` on timeout if specified. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +###### timeout + +`number` + +##### Returns + +`Promise`\ + +#### Call Signature + +> **wait**\<`K`\>(`event`, `predicate`, `timeout`): `Promise`\ + +Defined in: [src/api.ts:216](../src/api.ts#L216) + +Waits for specific event, with an optional predicate. +Returns `undefined` on timeout if specified. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +###### predicate + +(`event`) => `boolean` | `undefined` + +###### timeout + +`number` + +##### Returns + +`Promise`\ + +*** + +### init() + +> `static` **init**(`dbFilePrefix`, `dbKey?`, `confirm?`): `Promise`\<`ChatApi`\> + +Defined in: [src/api.ts:76](../src/api.ts#L76) + +Initializes the ChatApi. + +#### Parameters + +##### dbFilePrefix + +`string` + +File prefix for the database files. + +##### dbKey? + +`string` = `""` + +Database encryption key. + +##### confirm? + +[`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) = `core.MigrationConfirmation.YesUp` + +Migration confirmation mode. + +#### Returns + +`Promise`\<`ChatApi`\> diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md new file mode 100644 index 0000000000..a4955cb3d9 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md @@ -0,0 +1,205 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / ChatCommandError + +# Class: ChatCommandError + +Defined in: [src/api.ts:5](../src/api.ts#L5) + +## Extends + +- `Error` + +## Constructors + +### Constructor + +> **new ChatCommandError**(`message`, `response`): `ChatCommandError` + +Defined in: [src/api.ts:6](../src/api.ts#L6) + +#### Parameters + +##### message + +`string` + +##### response + +`ChatResponse` + +#### Returns + +`ChatCommandError` + +#### Overrides + +`Error.constructor` + +## Properties + +### message + +> **message**: `string` + +Defined in: [src/api.ts:6](../src/api.ts#L6) + +#### Inherited from + +`Error.message` + +*** + +### name + +> **name**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076) + +#### Inherited from + +`Error.name` + +*** + +### response + +> **response**: `ChatResponse` + +Defined in: [src/api.ts:6](../src/api.ts#L6) + +*** + +### stack? + +> `optional` **stack**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) + +#### Inherited from + +`Error.stack` + +*** + +### stackTraceLimit + +> `static` **stackTraceLimit**: `number` + +Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67) + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +`Error.stackTraceLimit` + +## Methods + +### captureStackTrace() + +> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` + +Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51) + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +`Error.captureStackTrace` + +*** + +### prepareStackTrace() + +> `static` **prepareStackTrace**(`err`, `stackTraces`): `any` + +Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55) + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +`Error.prepareStackTrace` diff --git a/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md b/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md new file mode 100644 index 0000000000..dcabb85b67 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / ConnReqType + +# Enumeration: ConnReqType + +Defined in: [src/api.ts:15](../src/api.ts#L15) + +Connection request types. + +## Enumeration Members + +### Contact + +> **Contact**: `"contact"` + +Defined in: [src/api.ts:17](../src/api.ts#L17) + +*** + +### Invitation + +> **Invitation**: `"invitation"` + +Defined in: [src/api.ts:16](../src/api.ts#L16) diff --git a/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md b/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md new file mode 100644 index 0000000000..efb4a75e81 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md @@ -0,0 +1,60 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / BotAddressSettings + +# Interface: BotAddressSettings + +Defined in: [src/api.ts:23](../src/api.ts#L23) + +Bot address settings. + +## Properties + +### autoAccept? + +> `optional` **autoAccept**: `boolean` + +Defined in: [src/api.ts:28](../src/api.ts#L28) + +Automatically accept contact requests. + +#### Default + +```ts +true +``` + +*** + +### businessAddress? + +> `optional` **businessAddress**: `boolean` + +Defined in: [src/api.ts:41](../src/api.ts#L41) + +Business contact address. +For all requests business chats will be created where other participants can be added. + +#### Default + +```ts +false +``` + +*** + +### welcomeMessage? + +> `optional` **welcomeMessage**: `string` \| `MsgContent` + +Defined in: [src/api.ts:34](../src/api.ts#L34) + +Optional welcome message to show before connection to the users. + +#### Default + +```ts +undefined (no welcome message) +``` diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md new file mode 100644 index 0000000000..6197befc8a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / EventSubscriberFunc + +# Type Alias: EventSubscriberFunc()\ + +> **EventSubscriberFunc**\<`K`\> = (`event`) => `void` \| `Promise`\<`void`\> + +Defined in: [src/api.ts:50](../src/api.ts#L50) + +## Type Parameters + +### K + +`K` *extends* `CEvt.Tag` + +## Parameters + +### event + +`ChatEvent` & \{ `type`: `K`; \} + +## Returns + +`void` \| `Promise`\<`void`\> diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md new file mode 100644 index 0000000000..3b63d11bd4 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / EventSubscribers + +# Type Alias: EventSubscribers + +> **EventSubscribers** = `{ [K in CEvt.Tag]?: EventSubscriberFunc }` + +Defined in: [src/api.ts:52](../src/api.ts#L52) diff --git a/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md b/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md new file mode 100644 index 0000000000..f076ee0fad --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / defaultBotAddressSettings + +# Variable: defaultBotAddressSettings + +> `const` **defaultBotAddressSettings**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md) + +Defined in: [src/api.ts:44](../src/api.ts#L44) diff --git a/packages/simplex-chat-nodejs/docs/bot.Function.run.md b/packages/simplex-chat-nodejs/docs/bot.Function.run.md new file mode 100644 index 0000000000..bc31ad01a8 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.Function.run.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / run + +# Function: run() + +> **run**(`__namedParameters`): `Promise`\<\[[`ChatApi`](api.Class.ChatApi.md), `User`, `UserContactLink` \| `undefined`\]\> + +Defined in: [src/bot.ts:49](../src/bot.ts#L49) + +## Parameters + +### \_\_namedParameters + +[`BotConfig`](bot.Interface.BotConfig.md) + +## Returns + +`Promise`\<\[[`ChatApi`](api.Class.ChatApi.md), `User`, `UserContactLink` \| `undefined`\]\> diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md new file mode 100644 index 0000000000..0951c5a129 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md @@ -0,0 +1,75 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotConfig + +# Interface: BotConfig + +Defined in: [src/bot.ts:37](../src/bot.ts#L37) + +## Properties + +### dbOpts + +> **dbOpts**: [`BotDbOpts`](bot.Interface.BotDbOpts.md) + +Defined in: [src/bot.ts:39](../src/bot.ts#L39) + +*** + +### events? + +> `optional` **events**: [`EventSubscribers`](api.TypeAlias.EventSubscribers.md) + +Defined in: [src/bot.ts:46](../src/bot.ts#L46) + +*** + +### onCommands? + +> `optional` **onCommands**: \{\[`key`: `string`\]: (`chatItem`, `command`) => `void` \| `Promise`\<`void`\> \| `undefined`; \} + +Defined in: [src/bot.ts:43](../src/bot.ts#L43) + +#### Index Signature + +\[`key`: `string`\]: (`chatItem`, `command`) => `void` \| `Promise`\<`void`\> \| `undefined` + +*** + +### onMessage()? + +> `optional` **onMessage**: (`chatItem`, `content`) => `void` \| `Promise`\<`void`\> + +Defined in: [src/bot.ts:41](../src/bot.ts#L41) + +#### Parameters + +##### chatItem + +`AChatItem` + +##### content + +`MsgContent` + +#### Returns + +`void` \| `Promise`\<`void`\> + +*** + +### options + +> **options**: [`BotOptions`](bot.Interface.BotOptions.md) + +Defined in: [src/bot.ts:40](../src/bot.ts#L40) + +*** + +### profile + +> **profile**: `Profile` + +Defined in: [src/bot.ts:38](../src/bot.ts#L38) diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md new file mode 100644 index 0000000000..7a9f113f6a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotDbOpts + +# Interface: BotDbOpts + +Defined in: [src/bot.ts:7](../src/bot.ts#L7) + +## Properties + +### confirmMigrations? + +> `optional` **confirmMigrations**: [`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) + +Defined in: [src/bot.ts:10](../src/bot.ts#L10) + +*** + +### dbFilePrefix + +> **dbFilePrefix**: `string` + +Defined in: [src/bot.ts:8](../src/bot.ts#L8) + +*** + +### dbKey? + +> `optional` **dbKey**: `string` + +Defined in: [src/bot.ts:9](../src/bot.ts#L9) diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md new file mode 100644 index 0000000000..44d4380e5a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md @@ -0,0 +1,81 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotOptions + +# Interface: BotOptions + +Defined in: [src/bot.ts:13](../src/bot.ts#L13) + +## Properties + +### addressSettings? + +> `optional` **addressSettings**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md) + +Defined in: [src/bot.ts:17](../src/bot.ts#L17) + +*** + +### allowFiles? + +> `optional` **allowFiles**: `boolean` + +Defined in: [src/bot.ts:18](../src/bot.ts#L18) + +*** + +### commands? + +> `optional` **commands**: `ChatBotCommand`[] + +Defined in: [src/bot.ts:19](../src/bot.ts#L19) + +*** + +### createAddress? + +> `optional` **createAddress**: `boolean` + +Defined in: [src/bot.ts:14](../src/bot.ts#L14) + +*** + +### logContacts? + +> `optional` **logContacts**: `boolean` + +Defined in: [src/bot.ts:21](../src/bot.ts#L21) + +*** + +### logNetwork? + +> `optional` **logNetwork**: `boolean` + +Defined in: [src/bot.ts:22](../src/bot.ts#L22) + +*** + +### updateAddress? + +> `optional` **updateAddress**: `boolean` + +Defined in: [src/bot.ts:15](../src/bot.ts#L15) + +*** + +### updateProfile? + +> `optional` **updateProfile**: `boolean` + +Defined in: [src/bot.ts:16](../src/bot.ts#L16) + +*** + +### useBotProfile? + +> `optional` **useBotProfile**: `boolean` + +Defined in: [src/bot.ts:20](../src/bot.ts#L20) diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md new file mode 100644 index 0000000000..c6082f2985 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md @@ -0,0 +1,205 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / ChatAPIError + +# Class: ChatAPIError + +Defined in: [src/core.ts:92](../src/core.ts#L92) + +## Extends + +- `Error` + +## Constructors + +### Constructor + +> **new ChatAPIError**(`message`, `chatError`): `ChatAPIError` + +Defined in: [src/core.ts:93](../src/core.ts#L93) + +#### Parameters + +##### message + +`string` + +##### chatError + +`ChatError` | `undefined` + +#### Returns + +`ChatAPIError` + +#### Overrides + +`Error.constructor` + +## Properties + +### chatError + +> **chatError**: `ChatError` \| `undefined` = `undefined` + +Defined in: [src/core.ts:93](../src/core.ts#L93) + +*** + +### message + +> **message**: `string` + +Defined in: [src/core.ts:93](../src/core.ts#L93) + +#### Inherited from + +`Error.message` + +*** + +### name + +> **name**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076) + +#### Inherited from + +`Error.name` + +*** + +### stack? + +> `optional` **stack**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) + +#### Inherited from + +`Error.stack` + +*** + +### stackTraceLimit + +> `static` **stackTraceLimit**: `number` + +Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67) + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +`Error.stackTraceLimit` + +## Methods + +### captureStackTrace() + +> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` + +Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51) + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +`Error.captureStackTrace` + +*** + +### prepareStackTrace() + +> `static` **prepareStackTrace**(`err`, `stackTraces`): `any` + +Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55) + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +`Error.prepareStackTrace` diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md new file mode 100644 index 0000000000..eff5123fb0 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md @@ -0,0 +1,205 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / ChatInitError + +# Class: ChatInitError + +Defined in: [src/core.ts:116](../src/core.ts#L116) + +## Extends + +- `Error` + +## Constructors + +### Constructor + +> **new ChatInitError**(`message`, `dbMigrationError`): `ChatInitError` + +Defined in: [src/core.ts:117](../src/core.ts#L117) + +#### Parameters + +##### message + +`string` + +##### dbMigrationError + +[`DBMigrationError`](core.TypeAlias.DBMigrationError.md) + +#### Returns + +`ChatInitError` + +#### Overrides + +`Error.constructor` + +## Properties + +### dbMigrationError + +> **dbMigrationError**: [`DBMigrationError`](core.TypeAlias.DBMigrationError.md) + +Defined in: [src/core.ts:117](../src/core.ts#L117) + +*** + +### message + +> **message**: `string` + +Defined in: [src/core.ts:117](../src/core.ts#L117) + +#### Inherited from + +`Error.message` + +*** + +### name + +> **name**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076) + +#### Inherited from + +`Error.name` + +*** + +### stack? + +> `optional` **stack**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) + +#### Inherited from + +`Error.stack` + +*** + +### stackTraceLimit + +> `static` **stackTraceLimit**: `number` + +Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67) + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +`Error.stackTraceLimit` + +## Methods + +### captureStackTrace() + +> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` + +Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51) + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +`Error.captureStackTrace` + +*** + +### prepareStackTrace() + +> `static` **prepareStackTrace**(`err`, `stackTraces`): `any` + +Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55) + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +`Error.prepareStackTrace` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md new file mode 100644 index 0000000000..02cf84b763 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md @@ -0,0 +1,41 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorMigration + +# Interface: ErrorMigration + +Defined in: [src/core.ts:144](../src/core.ts#L144) + +## Extends + +- `Interface` + +## Properties + +### dbFile + +> **dbFile**: `string` + +Defined in: [src/core.ts:146](../src/core.ts#L146) + +*** + +### migrationError + +> **migrationError**: [`MigrationError`](core.TypeAlias.MigrationError.md) + +Defined in: [src/core.ts:147](../src/core.ts#L147) + +*** + +### type + +> **type**: `"errorMigration"` + +Defined in: [src/core.ts:145](../src/core.ts#L145) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md new file mode 100644 index 0000000000..18e2429081 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorNotADatabase + +# Interface: ErrorNotADatabase + +Defined in: [src/core.ts:139](../src/core.ts#L139) + +## Extends + +- `Interface` + +## Properties + +### dbFile + +> **dbFile**: `string` + +Defined in: [src/core.ts:141](../src/core.ts#L141) + +*** + +### type + +> **type**: `"errorNotADatabase"` + +Defined in: [src/core.ts:140](../src/core.ts#L140) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md new file mode 100644 index 0000000000..4d85b04197 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md @@ -0,0 +1,41 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorSQL + +# Interface: ErrorSQL + +Defined in: [src/core.ts:150](../src/core.ts#L150) + +## Extends + +- `Interface` + +## Properties + +### dbFile + +> **dbFile**: `string` + +Defined in: [src/core.ts:152](../src/core.ts#L152) + +*** + +### migrationSQLError + +> **migrationSQLError**: `string` + +Defined in: [src/core.ts:153](../src/core.ts#L153) + +*** + +### type + +> **type**: `"errorSQL"` + +Defined in: [src/core.ts:151](../src/core.ts#L151) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md new file mode 100644 index 0000000000..34dea63aed --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / InvalidConfirmation + +# Interface: InvalidConfirmation + +Defined in: [src/core.ts:135](../src/core.ts#L135) + +## Extends + +- `Interface` + +## Properties + +### type + +> **type**: `"invalidConfirmation"` + +Defined in: [src/core.ts:136](../src/core.ts#L136) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md new file mode 100644 index 0000000000..a3ef341601 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / Tag + +# Type Alias: Tag + +> **Tag** = `"invalidConfirmation"` \| `"errorNotADatabase"` \| `"errorMigration"` \| `"errorSQL"` + +Defined in: [src/core.ts:129](../src/core.ts#L129) diff --git a/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md new file mode 100644 index 0000000000..7dfd4991bf --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md @@ -0,0 +1,43 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationConfirmation + +# Enumeration: MigrationConfirmation + +Defined in: [src/core.ts:101](../src/core.ts#L101) + +Migration confirmation mode + +## Enumeration Members + +### Console + +> **Console**: `"console"` + +Defined in: [src/core.ts:104](../src/core.ts#L104) + +*** + +### Error + +> **Error**: `"error"` + +Defined in: [src/core.ts:105](../src/core.ts#L105) + +*** + +### YesUp + +> **YesUp**: `"yesUp"` + +Defined in: [src/core.ts:102](../src/core.ts#L102) + +*** + +### YesUpDown + +> **YesUpDown**: `"yesUpDown"` + +Defined in: [src/core.ts:103](../src/core.ts#L103) diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md new file mode 100644 index 0000000000..deeb3213fd --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md @@ -0,0 +1,23 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatCloseStore + +# Function: chatCloseStore() + +> **chatCloseStore**(`ctrl`): `Promise`\<`void`\> + +Defined in: [src/core.ts:17](../src/core.ts#L17) + +Close chat store + +## Parameters + +### ctrl + +`bigint` + +## Returns + +`Promise`\<`void`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md new file mode 100644 index 0000000000..434aeeaae8 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatDecryptFile + +# Function: chatDecryptFile() + +> **chatDecryptFile**(`fromPath`, `__namedParameters`, `toPath`): `Promise`\<`void`\> + +Defined in: [src/core.ts:73](../src/core.ts#L73) + +Decrypt file + +## Parameters + +### fromPath + +`string` + +### \_\_namedParameters + +[`CryptoArgs`](core.Interface.CryptoArgs.md) + +### toPath + +`string` + +## Returns + +`Promise`\<`void`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md new file mode 100644 index 0000000000..6aa0ad2923 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatEncryptFile + +# Function: chatEncryptFile() + +> **chatEncryptFile**(`ctrl`, `fromPath`, `toPath`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> + +Defined in: [src/core.ts:65](../src/core.ts#L65) + +Encrypt file + +## Parameters + +### ctrl + +`bigint` + +### fromPath + +`string` + +### toPath + +`string` + +## Returns + +`Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md new file mode 100644 index 0000000000..9116026f56 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatMigrateInit + +# Function: chatMigrateInit() + +> **chatMigrateInit**(`dbPath`, `dbKey`, `confirm`): `Promise`\<`bigint`\> + +Defined in: [src/core.ts:7](../src/core.ts#L7) + +Initialize chat controller + +## Parameters + +### dbPath + +`string` + +### dbKey + +`string` + +### confirm + +[`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) + +## Returns + +`Promise`\<`bigint`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md new file mode 100644 index 0000000000..27de43e63c --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatReadFile + +# Function: chatReadFile() + +> **chatReadFile**(`path`, `__namedParameters`): `Promise`\<`ArrayBuffer`\> + +Defined in: [src/core.ts:58](../src/core.ts#L58) + +Read buffer from encrypted file + +## Parameters + +### path + +`string` + +### \_\_namedParameters + +[`CryptoArgs`](core.Interface.CryptoArgs.md) + +## Returns + +`Promise`\<`ArrayBuffer`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md new file mode 100644 index 0000000000..9bf44d6523 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatRecvMsgWait + +# Function: chatRecvMsgWait() + +> **chatRecvMsgWait**(`ctrl`, `wait`): `Promise`\<`ChatEvent` \| `undefined`\> + +Defined in: [src/core.ts:37](../src/core.ts#L37) + +Receive chat event + +## Parameters + +### ctrl + +`bigint` + +### wait + +`number` + +## Returns + +`Promise`\<`ChatEvent` \| `undefined`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md new file mode 100644 index 0000000000..2dfcba45b4 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatSendCmd + +# Function: chatSendCmd() + +> **chatSendCmd**(`ctrl`, `cmd`): `Promise`\<`ChatResponse`\> + +Defined in: [src/core.ts:25](../src/core.ts#L25) + +Send chat command as string + +## Parameters + +### ctrl + +`bigint` + +### cmd + +`string` + +## Returns + +`Promise`\<`ChatResponse`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md new file mode 100644 index 0000000000..3b1d770fbb --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatWriteFile + +# Function: chatWriteFile() + +> **chatWriteFile**(`ctrl`, `path`, `buffer`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> + +Defined in: [src/core.ts:50](../src/core.ts#L50) + +Write buffer to encrypted file + +## Parameters + +### ctrl + +`bigint` + +### path + +`string` + +### buffer + +`ArrayBuffer` + +## Returns + +`Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md new file mode 100644 index 0000000000..906ef3ec3e --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / APIResult + +# Interface: APIResult\ + +Defined in: [src/core.ts:87](../src/core.ts#L87) + +## Type Parameters + +### R + +`R` + +## Properties + +### error? + +> `optional` **error**: `ChatError` + +Defined in: [src/core.ts:89](../src/core.ts#L89) + +*** + +### result? + +> `optional` **result**: `R` + +Defined in: [src/core.ts:88](../src/core.ts#L88) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md new file mode 100644 index 0000000000..eddcb0bc5a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / CryptoArgs + +# Interface: CryptoArgs + +Defined in: [src/core.ts:111](../src/core.ts#L111) + +File encryption key and nonce + +## Properties + +### fileKey + +> **fileKey**: `string` + +Defined in: [src/core.ts:112](../src/core.ts#L112) + +*** + +### fileNonce + +> **fileNonce**: `string` + +Defined in: [src/core.ts:113](../src/core.ts#L113) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md new file mode 100644 index 0000000000..32f6c267aa --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / UpMigration + +# Interface: UpMigration + +Defined in: [src/core.ts:185](../src/core.ts#L185) + +## Properties + +### upName + +> **upName**: `string` + +Defined in: [src/core.ts:186](../src/core.ts#L186) + +*** + +### withDown + +> **withDown**: `boolean` + +Defined in: [src/core.ts:187](../src/core.ts#L187) diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md new file mode 100644 index 0000000000..8dab81a3a3 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / MTREDifferent + +# Interface: MTREDifferent + +Defined in: [src/core.ts:206](../src/core.ts#L206) + +## Extends + +- `Interface` + +## Properties + +### downMigrations + +> **downMigrations**: `string`[] + +Defined in: [src/core.ts:208](../src/core.ts#L208) + +*** + +### type + +> **type**: `"different"` + +Defined in: [src/core.ts:207](../src/core.ts#L207) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md new file mode 100644 index 0000000000..1de634e40e --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / MTRENoDown + +# Interface: MTRENoDown + +Defined in: [src/core.ts:201](../src/core.ts#L201) + +## Extends + +- `Interface` + +## Properties + +### type + +> **type**: `"noDown"` + +Defined in: [src/core.ts:202](../src/core.ts#L202) + +#### Overrides + +`Interface.type` + +*** + +### upMigrations + +> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md) + +Defined in: [src/core.ts:203](../src/core.ts#L203) diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md new file mode 100644 index 0000000000..768baa0068 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / Tag + +# Type Alias: Tag + +> **Tag** = `"noDown"` \| `"different"` + +Defined in: [src/core.ts:195](../src/core.ts#L195) diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md new file mode 100644 index 0000000000..a2742cbff2 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MEDowngrade + +# Interface: MEDowngrade + +Defined in: [src/core.ts:174](../src/core.ts#L174) + +## Extends + +- `Interface` + +## Properties + +### downMigrations + +> **downMigrations**: `string`[] + +Defined in: [src/core.ts:176](../src/core.ts#L176) + +*** + +### type + +> **type**: `"downgrade"` + +Defined in: [src/core.ts:175](../src/core.ts#L175) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md new file mode 100644 index 0000000000..08fe7d56e3 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MEUpgrade + +# Interface: MEUpgrade + +Defined in: [src/core.ts:169](../src/core.ts#L169) + +## Extends + +- `Interface` + +## Properties + +### type + +> **type**: `"upgrade"` + +Defined in: [src/core.ts:170](../src/core.ts#L170) + +#### Overrides + +`Interface.type` + +*** + +### upMigrations + +> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md) + +Defined in: [src/core.ts:171](../src/core.ts#L171) diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md new file mode 100644 index 0000000000..cd811a5747 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MigrationError + +# Interface: MigrationError + +Defined in: [src/core.ts:179](../src/core.ts#L179) + +## Extends + +- `Interface` + +## Properties + +### mtrError + +> **mtrError**: [`MTRError`](core.TypeAlias.MTRError.md) + +Defined in: [src/core.ts:181](../src/core.ts#L181) + +*** + +### type + +> **type**: `"migrationError"` + +Defined in: [src/core.ts:180](../src/core.ts#L180) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md new file mode 100644 index 0000000000..5ef6e70b08 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / Tag + +# Type Alias: Tag + +> **Tag** = `"upgrade"` \| `"downgrade"` \| `"migrationError"` + +Defined in: [src/core.ts:163](../src/core.ts#L163) diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md new file mode 100644 index 0000000000..95ddfa5b24 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md @@ -0,0 +1,18 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / DBMigrationError + +# DBMigrationError + +## Interfaces + +- [ErrorMigration](core.DBMigrationError.Interface.ErrorMigration.md) +- [ErrorNotADatabase](core.DBMigrationError.Interface.ErrorNotADatabase.md) +- [ErrorSQL](core.DBMigrationError.Interface.ErrorSQL.md) +- [InvalidConfirmation](core.DBMigrationError.Interface.InvalidConfirmation.md) + +## Type Aliases + +- [Tag](core.DBMigrationError.TypeAlias.Tag.md) diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md new file mode 100644 index 0000000000..70baca917a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md @@ -0,0 +1,16 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MTRError + +# MTRError + +## Interfaces + +- [MTREDifferent](core.MTRError.Interface.MTREDifferent.md) +- [MTRENoDown](core.MTRError.Interface.MTRENoDown.md) + +## Type Aliases + +- [Tag](core.MTRError.TypeAlias.Tag.md) diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md new file mode 100644 index 0000000000..cc66142dbc --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md @@ -0,0 +1,17 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationError + +# MigrationError + +## Interfaces + +- [MEDowngrade](core.MigrationError.Interface.MEDowngrade.md) +- [MEUpgrade](core.MigrationError.Interface.MEUpgrade.md) +- [MigrationError](core.MigrationError.Interface.MigrationError.md) + +## Type Aliases + +- [Tag](core.MigrationError.TypeAlias.Tag.md) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md new file mode 100644 index 0000000000..6473b3ef60 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / DBMigrationError + +# Type Alias: DBMigrationError + +> **DBMigrationError** = [`InvalidConfirmation`](core.DBMigrationError.Interface.InvalidConfirmation.md) \| [`ErrorNotADatabase`](core.DBMigrationError.Interface.ErrorNotADatabase.md) \| [`ErrorMigration`](core.DBMigrationError.Interface.ErrorMigration.md) \| [`ErrorSQL`](core.DBMigrationError.Interface.ErrorSQL.md) + +Defined in: [src/core.ts:122](../src/core.ts#L122) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md new file mode 100644 index 0000000000..11aa5b7c24 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MTRError + +# Type Alias: MTRError + +> **MTRError** = [`MTRENoDown`](core.MTRError.Interface.MTRENoDown.md) \| [`MTREDifferent`](core.MTRError.Interface.MTREDifferent.md) + +Defined in: [src/core.ts:190](../src/core.ts#L190) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md new file mode 100644 index 0000000000..c15b679769 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationError + +# Type Alias: MigrationError + +> **MigrationError** = [`MEUpgrade`](core.MigrationError.Interface.MEUpgrade.md) \| [`MEDowngrade`](core.MigrationError.Interface.MEDowngrade.md) \| [`MigrationError`](core.MigrationError.Interface.MigrationError.md) + +Defined in: [src/core.ts:157](../src/core.ts#L157) diff --git a/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md b/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md new file mode 100644 index 0000000000..7fa5d81b7a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / botAddressSettings + +# Function: botAddressSettings() + +> **botAddressSettings**(`__namedParameters`): [`BotAddressSettings`](api.Interface.BotAddressSettings.md) + +Defined in: [src/util.ts:48](../src/util.ts#L48) + +## Parameters + +### \_\_namedParameters + +`UserContactLink` + +## Returns + +[`BotAddressSettings`](api.Interface.BotAddressSettings.md) diff --git a/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md new file mode 100644 index 0000000000..dd68403c40 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / chatInfoName + +# Function: chatInfoName() + +> **chatInfoName**(`cInfo`): `string` + +Defined in: [src/util.ts:18](../src/util.ts#L18) + +## Parameters + +### cInfo + +`ChatInfo` + +## Returns + +`string` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md new file mode 100644 index 0000000000..dccb2a9d29 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / chatInfoRef + +# Function: chatInfoRef() + +> **chatInfoRef**(`cInfo`): `ChatRef` \| `undefined` + +Defined in: [src/util.ts:4](../src/util.ts#L4) + +## Parameters + +### cInfo + +`ChatInfo` + +## Returns + +`ChatRef` \| `undefined` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md b/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md new file mode 100644 index 0000000000..bc7a66f163 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / ciBotCommand + +# Function: ciBotCommand() + +> **ciBotCommand**(`chatItem`): [`BotCommand`](util.Interface.BotCommand.md) \| `undefined` + +Defined in: [src/util.ts:78](../src/util.ts#L78) + +## Parameters + +### chatItem + +`ChatItem` + +## Returns + +[`BotCommand`](util.Interface.BotCommand.md) \| `undefined` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md b/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md new file mode 100644 index 0000000000..7ab0bc540c --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / ciContentText + +# Function: ciContentText() + +> **ciContentText**(`__namedParameters`): `string` \| `undefined` + +Defined in: [src/util.ts:64](../src/util.ts#L64) + +## Parameters + +### \_\_namedParameters + +`ChatItem` + +## Returns + +`string` \| `undefined` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md b/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md new file mode 100644 index 0000000000..3f6c7b9562 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / contactAddressStr + +# Function: contactAddressStr() + +> **contactAddressStr**(`link`): `string` + +Defined in: [src/util.ts:44](../src/util.ts#L44) + +## Parameters + +### link + +`CreatedConnLink` + +## Returns + +`string` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md b/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md new file mode 100644 index 0000000000..c32081b6cf --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / fromLocalProfile + +# Function: fromLocalProfile() + +> **fromLocalProfile**(`__namedParameters`): `Profile` + +Defined in: [src/util.ts:56](../src/util.ts#L56) + +## Parameters + +### \_\_namedParameters + +`LocalProfile` + +## Returns + +`Profile` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md b/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md new file mode 100644 index 0000000000..896dc74a12 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / reactionText + +# Function: reactionText() + +> **reactionText**(`reaction`): `string` + +Defined in: [src/util.ts:89](../src/util.ts#L89) + +## Parameters + +### reaction + +`ACIReaction` + +## Returns + +`string` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.senderName.md b/packages/simplex-chat-nodejs/docs/util.Function.senderName.md new file mode 100644 index 0000000000..6bacad97f6 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.senderName.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / senderName + +# Function: senderName() + +> **senderName**(`cInfo`, `chatDir`): `string` + +Defined in: [src/util.ts:37](../src/util.ts#L37) + +## Parameters + +### cInfo + +`ChatInfo` + +### chatDir + +`CIDirection` + +## Returns + +`string` diff --git a/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md b/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md new file mode 100644 index 0000000000..21c3ad72ba --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / BotCommand + +# Interface: BotCommand + +Defined in: [src/util.ts:72](../src/util.ts#L72) + +## Properties + +### keyword + +> **keyword**: `string` + +Defined in: [src/util.ts:73](../src/util.ts#L73) + +*** + +### params + +> **params**: `string` + +Defined in: [src/util.ts:74](../src/util.ts#L74) diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js new file mode 100644 index 0000000000..16d0678b64 --- /dev/null +++ b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js @@ -0,0 +1,17 @@ +(async () => { + const {bot} = await import("../dist/index.js") + const [chat, _user, _address] = await bot.run({ + profile: {displayName: "Squaring bot example", fullName: ""}, + dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + options: { + addressSettings: {welcomeMessage: "Send a number, I will square it."}, + }, + onMessage: async (ci, content) => { + const n = +content.text + const reply = typeof n === "number" && !isNaN(n) + ? `${n} * ${n} = ${n * n}` + : `this is not a number` + await chat.apiSendTextReply(ci, reply) + } + }) +})() diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot.ts b/packages/simplex-chat-nodejs/examples/squaring-bot.ts new file mode 100644 index 0000000000..682e7b887a --- /dev/null +++ b/packages/simplex-chat-nodejs/examples/squaring-bot.ts @@ -0,0 +1,42 @@ +import {T} from "@simplex-chat/types" +import {bot, util} from "../dist" + +(async () => { + const welcomeMessage = "Hello! I am a simple squaring bot.\n\nIf you send me a number, I will calculate its square." + const [chat, _user, _address] = await bot.run({ + profile: {displayName: "Squaring bot example", fullName: ""}, + dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + options: { + addressSettings: {autoAccept: true, welcomeMessage, businessAddress: false}, + commands: [ // commands to show in client UI + {type: "command", keyword: "help", label: "Send welcome message"}, + {type: "command", keyword: "info", label: "More information (not implemented)"} + ], + logContacts: true, + logNetwork: false + }, + onMessage: async (ci, content) => { + const n = +content.text + const reply = typeof n === "number" && !isNaN(n) + ? `${n} * ${n} = ${n * n}` + : `this is not a number` + await chat.apiSendTextReply(ci, reply) + }, + onCommands: { // command handlers can be different from commands to be shown in client UI + "help": async (ci: T.AChatItem, _cmd: util.BotCommand) => { + await chat.apiSendTextMessage(ci.chatInfo, welcomeMessage) + }, + // fallback handler that will be called for all other commands + "": async (ci: T.AChatItem, _cmd: util.BotCommand) => { + await chat.apiSendTextReply(ci, "This command is not supported") + } + }, + // If you use `onMessage` and subscribe to "newChatItems" event, exclude content messages from processing + // If you use `onCommands` and subscribe to "newChatItems" event, exclude commands from processing + events: { + "chatItemReaction": ({added, reaction}) => { + console.log(`${util.senderName(reaction.chatInfo, reaction.chatReaction.chatDir)} ${added ? "added" : "removed"} reaction ${util.reactionText(reaction)}`) + } + }, + }) +})() diff --git a/packages/simplex-chat-nodejs/jest.config.js b/packages/simplex-chat-nodejs/jest.config.js new file mode 100644 index 0000000000..18c5ddf0e5 --- /dev/null +++ b/packages/simplex-chat-nodejs/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: "ts-jest", + maxWorkers: 1, + testEnvironment: "node", + transform: { + '^.+\\.ts$': ['ts-jest', { + tsconfig: 'tests/tsconfig.json' + }] + } +} diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json new file mode 100644 index 0000000000..498d502edd --- /dev/null +++ b/packages/simplex-chat-nodejs/package.json @@ -0,0 +1,58 @@ +{ + "name": "simplex-chat", + "version": "6.5.0-beta.4.4", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "src", + "cpp", + "dist", + "binding.gyp", + "tsconfig.json", + "docs", + "examples" + ], + "scripts": { + "preinstall": "node src/download-libs.js", + "install": "node-gyp configure; node-gyp rebuild --release", + "install-tools": "npm install -g node-gyp", + "configure": "node-gyp configure; mkdir libs 2> /dev/null | true", + "build": "node-gyp rebuild && tsc && cp ./src/simplex.* ./dist", + "run": "node src/index.js", + "build-run": "node-gyp build && node src/index.js", + "test": "jest", + "docs": "typedoc" + }, + "dependencies": { + "@simplex-chat/types": "^0.3.0", + "extract-zip": "^2.0.1", + "fast-deep-equal": "^3.1.3", + "node-addon-api": "^8.5.0" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^25.0.5", + "jest": "^30.2.0", + "ts-jest": "^29.4.6", + "typedoc": "^0.28.15", + "typedoc-plugin-markdown": "^4.9.0", + "typescript": "^5.9.3" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/simplex-chat/simplex-chat.git" + }, + "keywords": [ + "messenger", + "chat", + "privacy", + "security" + ], + "author": "SimpleX Chat", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/simplex-chat/simplex-chat/issues" + }, + "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-nodejs#readme", + "description": "SimpleX Chat Node.js library for chat bots" +} diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts new file mode 100644 index 0000000000..c3e85b3915 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -0,0 +1,875 @@ +import {CC, CEvt, ChatEvent, ChatResponse, T} from "@simplex-chat/types" +import * as core from "./core" +import * as util from "./util" + +export class ChatCommandError extends Error { + constructor(public message: string, public response: ChatResponse) { + super(message) + } +} + +/** + * Connection request types. + * @enum {string} + */ +export enum ConnReqType { + Invitation = "invitation", + Contact = "contact", +} + +/** + * Bot address settings. + */ +export interface BotAddressSettings { + /** + * Automatically accept contact requests. + * @default true + */ + autoAccept?: boolean + + /** + * Optional welcome message to show before connection to the users. + * @default undefined (no welcome message) + */ + welcomeMessage?: T.MsgContent | string | undefined + + /** + * Business contact address. + * For all requests business chats will be created where other participants can be added. + * @default false + */ + businessAddress?: boolean +} + +export const defaultBotAddressSettings: BotAddressSettings = { + autoAccept: true, + welcomeMessage: undefined, + businessAddress: false +} + +export type EventSubscriberFunc = (event: ChatEvent & {type: K}) => void | Promise + +export type EventSubscribers = {[K in CEvt.Tag]?: EventSubscriberFunc} + +interface EventSubscriber { + subscriber: EventSubscriberFunc + once: boolean +} + +/** + * Main API class for interacting with the chat core library. + */ +export class ChatApi { + private receiveEvents = false + private eventsLoop: Promise | undefined = undefined + private subscribers: {[K in CEvt.Tag]?: EventSubscriber[]} = {} + private receivers: EventSubscriberFunc[] = [] + + private constructor(protected ctrl_: bigint | undefined) {} + + /** + * Initializes the ChatApi. + * @param {string} dbFilePrefix - File prefix for the database files. + * @param {string} [dbKey=""] - Database encryption key. + * @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode. + */ + static async init( + dbFilePrefix: string, + dbKey: string = "", + confirm = core.MigrationConfirmation.YesUp + ): Promise { + const ctrl = await core.chatMigrateInit(dbFilePrefix, dbKey, confirm) + return new ChatApi(ctrl) + } + + /** + * Start chat controller. Must be called with the existing user profile. + */ + async startChat(): Promise { + this.receiveEvents = true + this.eventsLoop = this.runEventsLoop() + const r = await this.sendChatCmd(CC.StartChat.cmdString({mainApp: true, enableSndFiles: true})) + if (r.type !== "chatStarted" && r.type !== "chatRunning") { + throw new ChatCommandError("error starting chat", r) + } + } + + /** + * Stop chat controller. + * Must be called before closing the database. + * Usually doesn't need to be called in chat bots. + */ + async stopChat(): Promise { + const r = await this.sendChatCmd("/_stop") + if (r.type !== "chatStopped") throw new ChatCommandError("error starting chat", r) + this.receiveEvents = false + if (this.eventsLoop) await this.eventsLoop + this.eventsLoop = undefined + } + + /** + * Close chat database. + * Usually doesn't need to be called in chat bots. + */ + async close(): Promise { + this.receiveEvents = false + if (this.eventsLoop) await this.eventsLoop + this.eventsLoop = undefined + await core.chatCloseStore(this.ctrl) + this.ctrl_ = undefined + } + + private async runEventsLoop(): Promise { + while (this.receiveEvents) { + try { + const event = await this.recvChatEvent() + if (!event) continue + const subs = this.subscribers[event.type] + if (subs) { + for (const {subscriber, once} of [...subs]) { + try { + const p = (subscriber as EventSubscriberFunc)(event) + if (p instanceof Promise) await p + } catch(e) { + console.log(`${event.type} event processing error`, e) + } + if (once) this.off(event.type, subscriber as EventSubscriberFunc) + } + } + for (const r of [...this.receivers]) { + try { + const p = r(event) + if (p instanceof Promise) await p + } catch(e) { + console.log(`${event.type} event processing error`, e) + } + } + } catch(err) { + const e = err as core.ChatAPIError + if ("chatError" in e) { + console.log("Chat error", e.chatError) + } else { + console.log("Invalid event", e) + } + } + } + } + + /** + * Subscribe multiple event handlers at once. + * @param subscribers - An object mapping event types (CEvt.Tag) to their subscriber functions. + * @throws {Error} If the same function is subscribed to event. + */ + on(subscribers: EventSubscribers): void + + /** + * Subscribe a handler to a specific event. + * @param {CEvt.Tag} event - The event type to subscribe to. + * @param subscriber - The subscriber function for the event. + * @throws {Error} If the same function is subscribed to event. + */ + on(event: K, subscriber: EventSubscriberFunc): void + on(events: K | EventSubscribers, subscriber?: EventSubscriberFunc): void { + if (typeof events === "string" && subscriber) { + this.on_(events, subscriber) + } else { + const eventEntries = Object.entries(events) as [CEvt.Tag, EventSubscriberFunc | undefined][] + for (const [event, subscriber] of eventEntries) { + if (subscriber) this.on_(event, subscriber) + } + } + } + + private on_(event: K, subscriber: EventSubscriberFunc, once: boolean = false): void { + const subs: EventSubscriber[] = this.subscribers[event] || (this.subscribers[event] = []) + if (subs.some(s => s.subscriber === subscriber)) throw Error(`this function is already subscribed to ${event}`) + subs.push({subscriber, once}) + } + + /** + * Subscribe a handler to any event. + * @param receiver - The receiver function for any event. + * @throws {Error} If the same function is subscribed to event. + */ + onAny(receiver: EventSubscriberFunc): void { + if (this.receivers.some(s => s === receiver)) throw Error("this function is already subscribed") + this.receivers.push(receiver) + } + + /** + * Subscribe a handler to a specific event to be delivered one time. + * @param {CEvt.Tag} event - The event type to subscribe to. + * @param subscriber - The subscriber function for the event. + * @throws {Error} If the same function is subscribed to event. + */ + once(event: K, subscriber: EventSubscriberFunc): void { + this.on_(event, subscriber, true) + } + + /** + * Waits for specific event, with an optional predicate. + * Returns `undefined` on timeout if specified. + */ + wait(event: K): Promise + wait(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined): Promise + wait(event: K, timeout: number): Promise + wait(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined, timeout: number): Promise + wait( + event: K, + predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined | number = undefined, // number for timeout + timeout: number = 0 // milliseconds, default - indefinite + ): Promise { + if (typeof predicate === "number") { + timeout = predicate + predicate = undefined + } + return new Promise((resolve, reject) => { + let done = false + const cleanup = () => { + done = true + this.off(event, subscriber) + } + const subscriber: EventSubscriberFunc = async (evt: ChatEvent & {type: K}) => { + if (done) return + if (predicate) { + try { if (!predicate(evt)) return } + catch(e) { cleanup(); reject(e); return } + } + cleanup() + resolve(evt) + } + this.on(event, subscriber) + if (timeout > 0) { + setTimeout(() => { if (!done) { cleanup(); resolve(undefined) } }, timeout) + } + }) + } + + /** + * Unsubscribe all or a specific handler from a specific event. + * @param {CEvt.Tag} event - The event type to unsubscribe from. + * @param subscriber - An optional subscriber function for the event. + */ + off(event: K, subscriber: EventSubscriberFunc | undefined = undefined): void { + if (subscriber) { + const subs = this.subscribers[event] + if (subs) { + const i = subs.findIndex(s => s.subscriber === subscriber) + if (i >= 0) subs.splice(i, 1) + } + } else { + delete this.subscribers[event] + } + } + + /** + * Unsubscribe all or a specific handler from any events. + * @param receiver - An optional subscriber function for the event. + */ + offAny(receiver: EventSubscriberFunc | undefined = undefined): void { + if (receiver) { + const i = this.receivers.findIndex(r => r === receiver) + if (i >= 0) this.receivers.splice(i, 1) + } else { + this.receivers = [] + } + } + + /** + * Chat controller is initialized + */ + get initialized(): boolean { + return typeof this.ctrl_ === "bigint" + } + + /** + * Chat controller is started + */ + get started(): boolean { + return this.receiveEvents && this.eventsLoop !== undefined + } + + /** + * Chat controller reference + */ + get ctrl(): bigint { + if (typeof this.ctrl_ === "bigint") return this.ctrl_ + else throw Error("chat api controller not initialized") + } + + async sendChatCmd(cmd: string): Promise { + return await core.chatSendCmd(this.ctrl, cmd) + } + + async recvChatEvent(wait: number = 5_000_000): Promise { + return await core.chatRecvMsgWait(this.ctrl, wait) + } + + /** + * Create bot address. + * Network usage: interactive. + */ + async apiCreateUserAddress(userId: number): Promise { + const r = await this.sendChatCmd(CC.APICreateMyAddress.cmdString({userId})) + if (r.type === "userContactLinkCreated") return r.connLinkContact + throw new ChatCommandError("error creating user address", r) + } + + /** + * Deletes a user address. + * Network usage: background. + */ + async apiDeleteUserAddress(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIDeleteMyAddress.cmdString({userId})) + if (r.type === "userContactLinkDeleted") return + throw new ChatCommandError("error deleting user address", r) + } + + /** + * Get bot address and settings. + * Network usage: no. + */ + async apiGetUserAddress(userId: number): Promise { + try { + const r = await this.sendChatCmd(CC.APIShowMyAddress.cmdString({userId})) + switch (r.type) { + case "userContactLink": return r.contactLink + default: throw new ChatCommandError("error loading user address", r) + } + } catch (err) { + const e = err as any + if (e.chatError?.type === "errorStore" && e.chatError.storeError?.type === "userContactLinkNotFound") return undefined + throw e + } + } + + /** + * Add address to bot profile. + * Network usage: interactive. + */ + async apiSetProfileAddress(userId: number, enable: boolean): Promise { + const r = await this.sendChatCmd(CC.APISetProfileAddress.cmdString({userId, enable})) + switch (r.type) { + case "userProfileUpdated": + return r.updateSummary + default: + throw new ChatCommandError("error loading user address", r) + } + } + + /** + * Set bot address settings. + * Network usage: interactive. + */ + async apiSetAddressSettings(userId: number, {autoAccept, welcomeMessage, businessAddress}: BotAddressSettings): Promise { + const autoReply = welcomeMessage || defaultBotAddressSettings.welcomeMessage + const settings: T.AddressSettings = { + autoAccept: (autoAccept === undefined ? defaultBotAddressSettings.autoAccept : autoAccept) ? {acceptIncognito: false} : undefined, + autoReply: typeof autoReply === "string" ? {type: "text", text: autoReply} : autoReply, + businessAddress: businessAddress || defaultBotAddressSettings.businessAddress || false + } + const r = await this.sendChatCmd(CC.APISetAddressSettings.cmdString({userId, settings})) + if (r.type !== "userContactLinkUpdated") { + throw new ChatCommandError("error changing user contact address settings", r) + } + } + + /** + * Send messages. + * Network usage: background. + */ + async apiSendMessages(chat: [T.ChatType, number] | T.ChatRef | T.ChatInfo, messages: T.ComposedMessage[], liveMessage = false): Promise { + const sendRef = Array.isArray(chat) + ? {chatType: chat[0], chatId: chat[1]} + : "chatType" in chat + ? chat + : util.chatInfoRef(chat) + if (!sendRef) throw Error("apiSendMessages: can't send messages to this chat") + const r = await this.sendChatCmd( + CC.APISendMessages.cmdString({ + sendRef, + composedMessages: messages, + liveMessage + }) + ) + if (r.type === "newChatItems") return r.chatItems + throw new ChatCommandError("unexpected response", r) + } + + /** + * Send text message. + * Network usage: background. + */ + async apiSendTextMessage(chat: [T.ChatType, number] | T.ChatRef | T.ChatInfo, text: string, inReplyTo?: number): Promise { + return this.apiSendMessages(chat, [{msgContent: {type: "text", text}, mentions: {}, quotedItemId: inReplyTo}]) + } + + /** + * Send text message in reply to received message. + * Network usage: background. + */ + async apiSendTextReply(chatItem: T.AChatItem, text: string): Promise { + return this.apiSendTextMessage(chatItem.chatInfo, text, chatItem.chatItem.meta.itemId) + } + + /** + * Update message. + * Network usage: background. + */ + async apiUpdateChatItem(chatType: T.ChatType, chatId: number, chatItemId: number, msgContent: T.MsgContent, liveMessage: false): Promise { + const r = await this.sendChatCmd( + CC.APIUpdateChatItem.cmdString({ + chatRef: {chatType, chatId}, + chatItemId, + liveMessage, + updatedMessage: {msgContent, mentions: {}}, + }) + ) + if (r.type === "chatItemUpdated") return r.chatItem.chatItem + throw new ChatCommandError("error updating chat item", r) + } + + /** + * Delete message. + * Network usage: background. + */ + async apiDeleteChatItems( + chatType: T.ChatType, + chatId: number, + chatItemIds: number[], + deleteMode: T.CIDeleteMode + ): Promise { + const r = await this.sendChatCmd(CC.APIDeleteChatItem.cmdString({chatRef: {chatType, chatId}, chatItemIds, deleteMode})) + if (r.type === "chatItemsDeleted") return r.chatItemDeletions + throw new ChatCommandError("error deleting chat item", r) + } + + /** + * Moderate message. Requires Moderator role (and higher than message author's). + * Network usage: background. + */ + async apiDeleteMemberChatItem(groupId: number, chatItemIds: number[]): Promise { + const r = await this.sendChatCmd(CC.APIDeleteMemberChatItem.cmdString({groupId, chatItemIds})) + if (r.type === "chatItemsDeleted") return r.chatItemDeletions + throw new ChatCommandError("error deleting member chat item", r) + } + + /** + * Add/remove message reaction. + * Network usage: background. + */ + async apiChatItemReaction( + chatType: T.ChatType, + chatId: number, + chatItemId: number, + add: boolean, + reaction: T.MsgReaction + ) { + const r = await this.sendChatCmd(CC.APIChatItemReaction.cmdString({chatRef: {chatType, chatId}, chatItemId, add, reaction})) + if (r.type === "chatItemsDeleted") return r.chatItemDeletions + throw new ChatCommandError("error setting item reaction", r) + } + + /** + * Receive file. + * Network usage: no. + */ + async apiReceiveFile(fileId: number): Promise { + const r = await this.sendChatCmd(CC.ReceiveFile.cmdString({fileId, userApprovedRelays: true})) + if (r.type === "rcvFileAccepted") return r.chatItem + throw new ChatCommandError("error receiving file", r) + } + + /** + * Cancel file. + * Network usage: background. + */ + async apiCancelFile(fileId: number): Promise { + const r = await this.sendChatCmd(CC.CancelFile.cmdString({fileId})) + if (r.type === "sndFileCancelled" || r.type === "rcvFileCancelled") return + throw new ChatCommandError("error canceling file", r) + } + + /** + * Add contact to group. Requires bot to have Admin role. + * Network usage: interactive. + */ + async apiAddMember(groupId: number, contactId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIAddMember.cmdString({groupId, contactId, memberRole})) + if (r.type === "sentGroupInvitation") return r.member + throw new ChatCommandError("error adding member", r) + } + + /** + * Join group. + * Network usage: interactive. + */ + async apiJoinGroup(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIJoinGroup.cmdString({groupId})) + if (r.type === "userAcceptedGroupSent") return r.groupInfo + throw new ChatCommandError("error joining group", r) + } + + /** + * Accept group member. Requires Admin role. + * Network usage: background. + */ + async apiAcceptMember(groupId: number, groupMemberId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIAcceptMember.cmdString({groupId, groupMemberId, memberRole})) + if (r.type === "memberAccepted") return r.member + throw new ChatCommandError("error accepting member", r) + } + + /** + * Set members role. Requires Admin role. + * Network usage: background. + */ + async apiSetMembersRole(groupId: number, groupMemberIds: number[], memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIMembersRole.cmdString({groupId, groupMemberIds, memberRole})) + if (r.type === "membersRoleUser") return + throw new ChatCommandError("error setting members role", r) + } + + /** + * Block members. Requires Moderator role. + * Network usage: background. + */ + async apiBlockMembersForAll(groupId: number, groupMemberIds: number[], blocked: boolean): Promise { + const r = await this.sendChatCmd(CC.APIBlockMembersForAll.cmdString({groupId, groupMemberIds, blocked})) + if (r.type === "membersBlockedForAllUser") return + throw new ChatCommandError("error blocking members", r) + } + + /** + * Remove members. Requires Admin role. + * Network usage: background. + */ + async apiRemoveMembers(groupId: number, memberIds: number[], withMessages = false): Promise { + const r = await this.sendChatCmd(CC.APIRemoveMembers.cmdString({groupId, groupMemberIds: memberIds, withMessages})) + if (r.type === "userDeletedMembers") return r.members + throw new ChatCommandError("error removing member", r) + } + + /** + * Leave group. + * Network usage: background. + */ + async apiLeaveGroup(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APILeaveGroup.cmdString({groupId})) + if (r.type === "leftMemberUser") return r.groupInfo + throw new ChatCommandError("error leaving group", r) + } + + /** + * Get group members. + * Network usage: no. + */ + async apiListMembers(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIListMembers.cmdString({groupId})) + if (r.type === "groupMembers") return r.group.members + throw new ChatCommandError("error getting group members", r) + } + + /** + * Create group. + * Network usage: no. + */ + async apiNewGroup(userId: number, groupProfile: T.GroupProfile): Promise { + const r = await this.sendChatCmd(CC.APINewGroup.cmdString({userId, groupProfile, incognito: false})) + if (r.type === "groupCreated") return r.groupInfo + throw new ChatCommandError("error creating group", r) + } + + /** + * Update group profile. + * Network usage: background. + */ + async apiUpdateGroupProfile(groupId: number, groupProfile: T.GroupProfile): Promise { + const r = await this.sendChatCmd(CC.APIUpdateGroupProfile.cmdString({groupId, groupProfile})) + if (r.type === "groupUpdated") return r.toGroup + throw new ChatCommandError("error updating group", r) + } + + /** + * Create group link. + * Network usage: interactive. + */ + async apiCreateGroupLink(groupId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APICreateGroupLink.cmdString({groupId, memberRole})) + if (r.type === "groupLinkCreated") { + const link = r.groupLink.connLinkContact + return link.connShortLink || link.connFullLink + } + throw new ChatCommandError("error creating group link", r) + } + + /** + * Set member role for group link. + * Network usage: no. + */ + async apiSetGroupLinkMemberRole(groupId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIGroupLinkMemberRole.cmdString({groupId, memberRole})) + if (r.type !== "groupLink") throw new ChatCommandError("error setting group link member role", r) + } + + /** + * Delete group link. + * Network usage: background. + */ + async apiDeleteGroupLink(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIDeleteGroupLink.cmdString({groupId})) + if (r.type !== "groupLinkDeleted") throw new ChatCommandError("error deleting group link", r) + } + + /** + * Get group link. + * Network usage: no. + */ + async apiGetGroupLink(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIGetGroupLink.cmdString({groupId})) + if (r.type === "groupLink") return r.groupLink + throw new ChatCommandError("error getting group link", r) + } + + async apiGetGroupLinkStr(groupId: number): Promise { + const link = (await this.apiGetGroupLink(groupId)).connLinkContact + return link.connShortLink || link.connFullLink + } + + /** + * Create 1-time invitation link. + * Network usage: interactive. + */ + async apiCreateLink(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIAddContact.cmdString({userId, incognito: false})) + if (r.type === "invitation") { + const link = r.connLinkInvitation + return link.connShortLink || link.connFullLink + } + throw new ChatCommandError("error creating link", r) + } + + /** + * Determine SimpleX link type and if the bot is already connected via this link. + * Network usage: interactive. + */ + async apiConnectPlan(userId: number, connectionLink: string): Promise<[T.ConnectionPlan, T.CreatedConnLink]> { + const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectionLink})) + if (r.type === "connectionPlan") return [r.connectionPlan, r.connLink] + throw new ChatCommandError("error getting connect plan", r) + } + + /** + * Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link + * Network usage: interactive. + */ + async apiConnect(userId: number, incognito: boolean, preparedLink?: T.CreatedConnLink): Promise { + const r = await this.sendChatCmd(CC.APIConnect.cmdString({userId, incognito, preparedLink_: preparedLink})) + return this.handleConnectResult(r) + } + + /** + * Connect via SimpleX link as string in the active user profile. + * Network usage: interactive. + */ + async apiConnectActiveUser(connLink: string): Promise { + const r = await this.sendChatCmd(CC.Connect.cmdString({incognito: false, connLink_: connLink})) + return this.handleConnectResult(r) + } + + private handleConnectResult(r: ChatResponse): ConnReqType { + switch (r.type) { + case "sentConfirmation": + return ConnReqType.Invitation + case "sentInvitation": + return ConnReqType.Contact + case "contactAlreadyExists": + throw new ChatCommandError("contact already exists", r) + default: + throw new ChatCommandError("connection error", r) + } + } + + /** + * Accept contact request. + * Network usage: interactive. + */ + async apiAcceptContactRequest(contactReqId: number): Promise { + const r = await this.sendChatCmd(CC.APIAcceptContact.cmdString({contactReqId})) + if (r.type === "acceptingContactRequest") return r.contact + throw new ChatCommandError("error accepting contact request", r) + } + + /** + * Reject contact request. The user who sent the request is **not notified**. + * Network usage: no. + */ + async apiRejectContactRequest(contactReqId: number): Promise { + const r = await this.sendChatCmd(CC.APIRejectContact.cmdString({contactReqId})) + if (r.type === "contactRequestRejected") return + throw new ChatCommandError("error rejecting contact request", r) + } + + /** + * Get contacts. + * Network usage: no. + */ + async apiListContacts(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIListContacts.cmdString({userId})) + if (r.type === "contactsList") return r.contacts + throw new ChatCommandError("error listing contacts", r) + } + + /** + * Get groups. + * Network usage: no. + */ + async apiListGroups(userId: number, contactId?: number, search?: string): Promise { + const r = await this.sendChatCmd(CC.APIListGroups.cmdString({userId, contactId_: contactId, search})) + if (r.type === "groupsList") return r.groups + throw new ChatCommandError("error listing groups", r) + } + + /** + * Delete chat. + * Network usage: background. + */ + async apiDeleteChat(chatType: T.ChatType, chatId: number, deleteMode: T.ChatDeleteMode = {type: "full", notify: true}): Promise { + const r = await this.sendChatCmd(CC.APIDeleteChat.cmdString({chatRef: {chatType, chatId}, chatDeleteMode: deleteMode})) + switch (chatType) { + case T.ChatType.Direct: + if (r.type === "contactDeleted") return + break + case T.ChatType.Group: + if (r.type === "groupDeletedUser") return + break + } + throw new ChatCommandError("error deleting chat", r) + } + + /** + * Set group custom data. + * Network usage: no. + */ + async apiSetGroupCustomData(groupId: number, customData?: object): Promise { + const r = await this.sendChatCmd(CC.APISetGroupCustomData.cmdString({groupId, customData})) + if (r.type === "cmdOk") return + throw new ChatCommandError("error setting group custom data", r) + } + + /** + * Set contact custom data. + * Network usage: no. + */ + async apiSetContactCustomData(contactId: number, customData?: object): Promise { + const r = await this.sendChatCmd(CC.APISetContactCustomData.cmdString({contactId, customData})) + if (r.type === "cmdOk") return + throw new ChatCommandError("error setting contact custom data", r) + } + + /** + * Set auto-accept member contacts. + * Network usage: no. + */ + async apiSetAutoAcceptMemberContacts(userId: number, onOff: boolean): Promise { + const r = await this.sendChatCmd(CC.APISetUserAutoAcceptMemberContacts.cmdString({userId, onOff})) + if (r.type === "cmdOk") return + throw new ChatCommandError("error setting auto-accept member contacts", r) + } + + /** + * Get chat items. + * Network usage: no. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async apiGetChat(chatType: T.ChatType, chatId: number, count: number): Promise { + const r: any = await this.sendChatCmd(`/_get chat ${T.ChatType.cmdString(chatType)}${chatId} count=${count}`) + if (r.type === "apiChat") return r.chat + throw new ChatCommandError("error getting chat", r) + } + + /** + * Get active user profile + * Network usage: no. + */ + async apiGetActiveUser(): Promise { + try { + const r = await this.sendChatCmd(CC.ShowActiveUser.cmdString({})) + switch (r.type) { + case "activeUser": + return r.user + default: + throw new ChatCommandError("unexpected response", r) + } + } catch (err) { + const e = err as core.ChatAPIError + if (e.chatError?.type === "error" && e.chatError.errorType.type === "noActiveUser") return undefined + throw err + } + } + + /** + * Create new user profile + * Network usage: no. + */ + async apiCreateActiveUser(profile?: T.Profile): Promise { + const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false}})) + if (r.type === "activeUser") return r.user + throw new ChatCommandError("unexpected response", r) + } + + /** + * Get all user profiles + * Network usage: no. + */ + async apiListUsers(): Promise { + const r = await this.sendChatCmd(CC.ListUsers.cmdString({})) + if (r.type === "usersList") return r.users + throw new ChatCommandError("error listing users", r) + } + + /** + * Set active user profile + * Network usage: no. + */ + async apiSetActiveUser(userId: number, viewPwd?: string): Promise { + const r = await this.sendChatCmd(CC.APISetActiveUser.cmdString({userId, viewPwd})) + if (r.type === "activeUser") return r.user + throw new ChatCommandError("error setting active user", r) + } + + /** + * Delete user profile. + * Network usage: background. + */ + async apiDeleteUser(userId: number, delSMPQueues: boolean, viewPwd?: string): Promise { + const r = await this.sendChatCmd(CC.APIDeleteUser.cmdString({userId, delSMPQueues, viewPwd})) + if (r.type === "cmdOk") return + throw new ChatCommandError("error deleting user", r) + } + + /** + * Update user profile. + * Network usage: background. + */ + async apiUpdateProfile(userId: number, profile: T.Profile): Promise { + const r = await this.sendChatCmd(CC.APIUpdateProfile.cmdString({userId, profile})) + switch (r.type) { + case "userProfileNoChange": + return undefined + case "userProfileUpdated": + return r.updateSummary + default: + throw new ChatCommandError("error updating profile", r) + } + } + + /** + * Configure chat preference overrides for the contact. + * Network usage: background. + */ + async apiSetContactPrefs(contactId: number, preferences: T.Preferences): Promise { + const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences})) + if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r) + } +} diff --git a/packages/simplex-chat-nodejs/src/bot.ts b/packages/simplex-chat-nodejs/src/bot.ts new file mode 100644 index 0000000000..95a0c13d96 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/bot.ts @@ -0,0 +1,216 @@ +import {T} from "@simplex-chat/types" +import * as api from "./api" +import * as core from "./core" +import * as util from "./util" +import equal = require("fast-deep-equal") + +export interface BotDbOpts { + dbFilePrefix: string // two schema files will be named _chat.db and _agent.db + dbKey?: string + confirmMigrations?: core.MigrationConfirmation +} + +export interface BotOptions { + createAddress?: boolean + updateAddress?: boolean + updateProfile?: boolean + addressSettings?: api.BotAddressSettings + allowFiles?: boolean + commands?: T.ChatBotCommand[] // commands to show in client UI + useBotProfile?: boolean // create profile not marked as a bot, with default preferences + logContacts?: boolean + logNetwork?: boolean +} + +const defaultOpts: Required = { + createAddress: true, + updateAddress: true, + updateProfile: true, + addressSettings: api.defaultBotAddressSettings, + allowFiles: false, + commands: [], + useBotProfile: true, + logContacts: true, + logNetwork: false +} + +export interface BotConfig { + profile: T.Profile, + dbOpts: BotDbOpts, + options: BotOptions, + onMessage?: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise, + // command handlers can be different from commands to be shown in client UI + onCommands?: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise)}, + // If you use `onMessage` and to subscribe "newChatItems" event, exclude content messages from processing + // If you use `onCommands` and to subscribe "newChatItems" event, exclude commands from processing + events?: api.EventSubscribers +} + +export async function run({profile, dbOpts, options = defaultOpts, onMessage, onCommands = {}, events = {}}: BotConfig): Promise<[api.ChatApi, T.User, T.UserContactLink | undefined]> { + const bot = await api.ChatApi.init(dbOpts.dbFilePrefix, dbOpts.dbKey || "", dbOpts.confirmMigrations || core.MigrationConfirmation.YesUp) + const opts = fullOptions(options) + if (onMessage) subscribeMessages(bot, onMessage) + if (Object.keys(onCommands).length > 0) subscribeCommands(bot, onCommands) + if (Object.keys(events).length > 0) bot.on(events) + subscribeLogEvents(bot, opts) + const botProfile = mkBotProfile(profile, opts) + const user = await createBotUser(bot, botProfile) + await bot.startChat() + const address = await createOrUpdateAddress(bot, user, opts) + if (address) { + const addressLink = util.contactAddressStr(address.connLinkContact) + console.log(`Bot address: ${addressLink}`) + if (opts.useBotProfile) botProfile.contactLink = addressLink + } + await updateBotUserProfile(bot, user, botProfile, opts) + return [bot, user, address] +} + +function fullOptions(options: BotOptions): Required { + const opts = { + createAddress: options.createAddress ?? defaultOpts.createAddress, + updateAddress: options.updateAddress ?? defaultOpts.updateAddress, + updateProfile: options.updateProfile ?? defaultOpts.updateProfile, + addressSettings: options.addressSettings ?? defaultOpts.addressSettings, + allowFiles: options.allowFiles ?? defaultOpts.allowFiles, + commands: options.commands ?? defaultOpts.commands, + useBotProfile: options.useBotProfile ?? defaultOpts.useBotProfile, + logContacts: options.logContacts ?? defaultOpts.logContacts, + logNetwork: options.logNetwork ?? defaultOpts.logNetwork + } + const welcomeMessage = opts.addressSettings.welcomeMessage ?? defaultOpts.addressSettings.welcomeMessage + opts.addressSettings = { + autoAccept: opts.addressSettings.autoAccept ?? defaultOpts.addressSettings.autoAccept, + welcomeMessage: typeof welcomeMessage === "string" ? {type: "text", text: welcomeMessage} : welcomeMessage, + businessAddress: opts.addressSettings.businessAddress ?? defaultOpts.addressSettings.businessAddress + } + return opts +} + +function mkBotProfile(profile: T.Profile, opts: Required): T.Profile { + if (opts.useBotProfile) { + const prefs = profile.preferences || {} + if (prefs.files || prefs.calls || prefs.voice || prefs.commands) { + console.log("Option useBotProfile is enabled and profile preferences used for files, calls, voice or commands, exiting") + process.exit() + } + prefs.files = {allow: opts.allowFiles ? T.FeatureAllowed.Yes : T.FeatureAllowed.No} + prefs.calls = {allow: T.FeatureAllowed.No} + prefs.voice = {allow: T.FeatureAllowed.No} + prefs.commands = opts.commands + profile.preferences = prefs + profile.peerType = T.ChatPeerType.Bot + } else if (opts.commands.length > 0) { + console.log("Option useBotProfile is disabled and commands are passed, exiting") + process.exit() + } + return profile +} + +function subscribeMessages(bot: api.ChatApi, onMessage: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise) { + bot.on("newChatItems", async ({chatItems}) => { + for (const ci of chatItems) { + if (ci.chatItem.content.type === "rcvMsgContent") { + try { + const p = onMessage(ci, ci.chatItem.content.msgContent) + if (p instanceof Promise) await p + } catch (e) { + console.log("message processing error", e) + } + } + } + }) +} + +function subscribeCommands(bot: api.ChatApi, commands: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise)}) { + bot.on("newChatItems", async (evt) => { + for (const ci of evt.chatItems) { + const cmd = util.ciBotCommand(ci.chatItem) + if (cmd) { + const cmdFunc = commands[cmd.keyword] || commands[""] + if (cmdFunc) { + try { + const p = cmdFunc(ci, cmd) + if (p instanceof Promise) await p + } catch(e) { + console.log(`${cmd} command processing error`, e) + } + } + } + } + }) +} + +function subscribeLogEvents(bot: api.ChatApi, opts: Required) { + if (opts.logContacts) { + bot.on({ + "contactConnected": ({contact}) => console.log(`${contact.profile.displayName} connected`), + "contactDeletedByContact": ({contact}) => console.log(`${contact.profile.displayName} deleted connection with bot`) + }) + } + if (opts.logNetwork) { + bot.on({ + "hostConnected": ({transportHost}) => console.log(`connected server ${transportHost}`), + "hostDisconnected": ({transportHost}) => console.log(`diconnected server ${transportHost}`), + "subscriptionStatus": ({subscriptionStatus, connections}) => console.log(`${connections.length} subscription(s) ${subscriptionStatus.type}`) + }) + } +} + +async function createBotUser(bot: api.ChatApi, profile: T.Profile): Promise { + let user = await bot.apiGetActiveUser() + if (!user) { + console.log("No active user in database, creating...") + user = await bot.apiCreateActiveUser(profile) + } + console.log("Bot user: ", user.profile.displayName) + return user +} + +async function createOrUpdateAddress(bot: api.ChatApi, user: T.User, opts: Required): Promise { + const {userId} = user + let address = await bot.apiGetUserAddress(userId) + if (!address) { + if (opts.createAddress) { + console.log("Bot has no address, creating...") + await bot.apiCreateUserAddress(userId) + address = await bot.apiGetUserAddress(userId) + if (!address) { + console.log("Failed reading created user address, exiting") + process.exit() + } + } else { + console.log("Warning: bot has no address") + return + } + } + + const addressSettings = opts.addressSettings || defaultOpts.addressSettings + if (!equal(util.botAddressSettings(address), addressSettings)) { + if (opts.updateAddress) { + console.log("Bot address settings changed, updating...") + await bot.apiSetAddressSettings(userId, addressSettings) + } else { + console.log("Bot address settings changed") + } + } + + return address +} + +async function updateBotUserProfile(bot: api.ChatApi, user: T.User, profile: T.Profile, opts: Required): Promise { + const {userId} = user + if (!equal(util.fromLocalProfile(user.profile), profile)) { + if (opts.updateProfile) { + console.log("Bot profile changed, updating...") + const summary = await bot.apiUpdateProfile(userId, profile) + console.log( + summary + ? `Bot profile updated: ${summary.updateSuccesses} updated contact(s), ${summary.updateFailures} failed contact update(s).` + : "Unexpected: profile did not change!" + ) + } else { + console.log("Bot profile changed") + } + } +} diff --git a/packages/simplex-chat-nodejs/src/core.ts b/packages/simplex-chat-nodejs/src/core.ts new file mode 100644 index 0000000000..949c2356af --- /dev/null +++ b/packages/simplex-chat-nodejs/src/core.ts @@ -0,0 +1,210 @@ +import {ChatEvent, ChatResponse, T} from "@simplex-chat/types" +import * as simplex from "./simplex" + +/** + * Initialize chat controller + */ +export async function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise { + const [ctrl, res] = await simplex.chat_migrate_init(dbPath, dbKey, confirm) + const json = JSON.parse(res) + if (json.type === 'ok') return ctrl + throw new ChatInitError("Database or migration error (see dbMigrationError property)", json as DBMigrationError) +} + +/** + * Close chat store + */ +export async function chatCloseStore(ctrl: bigint): Promise { + const res = await simplex.chat_close_store(ctrl) + if (res !== "") throw new Error(res) +} + +/** + * Send chat command as string + */ +export async function chatSendCmd(ctrl: bigint, cmd: string): Promise { + const res = await simplex.chat_send_cmd(ctrl, cmd) + const json = JSON.parse(res) as APIResult + // console.log(cmd.slice(0, 16), json.result?.type || json.error) + if (typeof json.result === 'object') return json.result + if (typeof json.error === 'object') throw new ChatAPIError("Chat command error (see chatError property)", json.error as T.ChatError) + throw new ChatAPIError("Invalid chat command result") +} + +/** + * Receive chat event + */ +export async function chatRecvMsgWait(ctrl: bigint, wait: number): Promise { + const res = await simplex.chat_recv_msg_wait(ctrl, wait) + if (res === "") return undefined + const json = JSON.parse(res) as APIResult + // if (json.result) console.log("event", json.result.type) + if (typeof json.result === 'object') return json.result + if (typeof json.error === 'object') throw new ChatAPIError("Chat event error (see chatError property)", json.error as T.ChatError) + throw new ChatAPIError("Invalid chat event") +} + +/** + * Write buffer to encrypted file + */ +export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise { + const res = await simplex.chat_write_file(ctrl, path, buffer) + return cryptoArgsResult(res) +} + +/** + * Read buffer from encrypted file + */ +export async function chatReadFile(path: string, {fileKey, fileNonce}: CryptoArgs): Promise { + return await simplex.chat_read_file(path, fileKey, fileNonce) +} + +/** + * Encrypt file + */ +export async function chatEncryptFile(ctrl: bigint, fromPath: string, toPath: string): Promise { + const res = await simplex.chat_encrypt_file(ctrl, fromPath, toPath) + return cryptoArgsResult(res) +} + +/** + * Decrypt file + */ +export async function chatDecryptFile(fromPath: string, {fileKey, fileNonce}: CryptoArgs, toPath: string): Promise { + const res = await simplex.chat_decrypt_file(fromPath, fileKey, fileNonce, toPath) + if (res !== "") throw new Error(res) +} + +function cryptoArgsResult(res: string): CryptoArgs { + const json = JSON.parse(res) + switch (json.type) { + case "result": return json.cryptoArgs as CryptoArgs + case "error": throw Error(json.writeError) + default: throw Error("unexpected chat_write_file result: " + res) + } +} + +export interface APIResult { + result?: R + error?: T.ChatError +} + +export class ChatAPIError extends Error { + constructor(public message: string, public chatError: T.ChatError | undefined = undefined) { + super(message) + } +} + +/** + * Migration confirmation mode + */ +export enum MigrationConfirmation { + YesUp = "yesUp", + YesUpDown = "yesUpDown", + Console = "console", + Error = "error" +} + +/** + * File encryption key and nonce + */ +export interface CryptoArgs { + fileKey: string + fileNonce: string +} + +export class ChatInitError extends Error { + constructor(public message: string, public dbMigrationError: DBMigrationError) { + super(message) + } +} + +export type DBMigrationError = + | DBMigrationError.InvalidConfirmation + | DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key + | DBMigrationError.ErrorMigration + | DBMigrationError.ErrorSQL + +export namespace DBMigrationError { + export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL" + + interface Interface { + type: Tag + } + + export interface InvalidConfirmation extends Interface { + type: "invalidConfirmation" + } + + export interface ErrorNotADatabase extends Interface { + type: "errorNotADatabase" + dbFile: string + } + + export interface ErrorMigration extends Interface { + type: "errorMigration" + dbFile: string + migrationError: MigrationError + } + + export interface ErrorSQL extends Interface { + type: "errorSQL" + dbFile: string + migrationSQLError: string + } +} + +export type MigrationError = + | MigrationError.MEUpgrade + | MigrationError.MEDowngrade + | MigrationError.MigrationError + +export namespace MigrationError { + export type Tag = "upgrade" | "downgrade" | "migrationError" + + interface Interface { + type: Tag + } + + export interface MEUpgrade extends Interface { + type: "upgrade" + upMigrations: UpMigration + } + + export interface MEDowngrade extends Interface { + type: "downgrade" + downMigrations: string[] + } + + export interface MigrationError extends Interface { + type: "migrationError" + mtrError: MTRError + } +} + +export interface UpMigration { + upName: string + withDown: boolean +} + +export type MTRError = + | MTRError.MTRENoDown + | MTRError.MTREDifferent + +export namespace MTRError { + export type Tag = "noDown" | "different" + + interface Interface { + type: Tag + } + + export interface MTRENoDown extends Interface { + type: "noDown" + upMigrations: UpMigration + } + + export interface MTREDifferent extends Interface { + type: "different" + downMigrations: string[] + } +} diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js new file mode 100644 index 0000000000..6b8f583155 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -0,0 +1,224 @@ +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const extract = require('extract-zip'); + +const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; +const RELEASE_TAG = 'v6.5.0-beta.4'; +const ROOT_DIR = process.cwd(); // Root of the package being installed +const LIBS_DIR = path.join(ROOT_DIR, 'libs') +const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt'); + +// Detect platform and architecture +function getPlatformInfo() { + const platform = process.platform; + const arch = process.arch; + + let platformName; + let archName; + + if (platform === 'linux') { + platformName = 'linux'; + } else if (platform === 'darwin') { + platformName = 'macos'; + } else if (platform === 'win32') { + platformName = 'windows'; + } else { + throw new Error(`Unsupported platform: ${platform}`); + } + + if (arch === 'x64') { + archName = 'x86_64'; + } else if (arch === 'arm64') { + archName = 'aarch64'; + } else { + throw new Error(`Unsupported architecture: ${arch}`); + } + + return { platformName, archName }; +} + +// Cleanup on libs version mismatch +function cleanLibsDirectory() { + if (fs.existsSync(LIBS_DIR)) { + console.log('Cleaning old libraries...'); + fs.rmSync(LIBS_DIR, { recursive: true, force: true }); + fs.mkdirSync(LIBS_DIR, { recursive: true }); + console.log('✓ Old libraries removed'); + } +} + +// Check if libraries are already installed with the correct version +function isAlreadyInstalled() { + if (!fs.existsSync(INSTALLED_FILE)) { + return false; + } + + try { + const installedVersion = fs.readFileSync(INSTALLED_FILE, 'utf-8').trim(); + if (installedVersion === RELEASE_TAG) { + console.log(`✓ Libraries version ${RELEASE_TAG} already installed`); + return true; + } else { + console.log(`Version mismatch: installed ${installedVersion}, need ${RELEASE_TAG}`); + cleanLibsDirectory(); + return false; + } + } catch (err) { + console.warn(`Could not read installed.txt: ${err.message}`); + return false; + } +} + +async function install() { + try { + // Check if already installed + if (isAlreadyInstalled()) { + return; + } + + const { platformName, archName } = getPlatformInfo(); + const repoName = GITHUB_REPO.split('/')[1]; + const zipFilename = `${repoName}-${platformName}-${archName}.zip`; + const ZIP_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}/${zipFilename}`; + const ZIP_PATH = path.join(ROOT_DIR, zipFilename); + const TEMP_EXTRACT_DIR = path.join(ROOT_DIR, '.temp-extract'); + + console.log(`Detected: ${platformName} ${archName}`); + console.log(`Downloading: ${zipFilename}`); + + // Create libs directory + if (!fs.existsSync(LIBS_DIR)) { + fs.mkdirSync(LIBS_DIR, { recursive: true }); + } + + // Download zip with error handling + await downloadFile(ZIP_URL, ZIP_PATH); + + // Extract to temporary directory + console.log('Extracting to temporary directory...'); + if (!fs.existsSync(TEMP_EXTRACT_DIR)) { + fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true }); + } + await extract(ZIP_PATH, { dir: TEMP_EXTRACT_DIR }); + + // Move libs folder contents to final location + console.log('Moving libraries to libs/...'); + const libsSourcePath = path.join(TEMP_EXTRACT_DIR, 'libs'); + + if (fs.existsSync(libsSourcePath)) { + // Copy all files from libs folder to LIBS_DIR + const files = fs.readdirSync(libsSourcePath); + files.forEach(file => { + const src = path.join(libsSourcePath, file); + const dest = path.join(LIBS_DIR, file); + + if (fs.statSync(src).isDirectory()) { + copyDirSync(src, dest); + } else { + fs.copyFileSync(src, dest); + } + }); + } else { + throw new Error('libs folder not found in zip archive'); + } + + // Write installed.txt with version + fs.writeFileSync(INSTALLED_FILE, RELEASE_TAG, 'utf-8'); + console.log(`✓ Wrote version ${RELEASE_TAG} to installed.txt`); + + // Cleanup + fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true }); + fs.unlinkSync(ZIP_PATH); + console.log('✓ Installation complete'); + } catch (err) { + console.error('✗ Failed:', err.message); + process.exit(1); + } +} + +// Helper function to recursively copy directories +function copyDirSync(src, dest) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + const files = fs.readdirSync(src); + files.forEach(file => { + const srcFile = path.join(src, file); + const destFile = path.join(dest, file); + if (fs.statSync(srcFile).isDirectory()) { + copyDirSync(srcFile, destFile); + } else { + fs.copyFileSync(srcFile, destFile); + } + }); +} + +function downloadFile(url, dest) { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(dest); + + https.get(url, { headers: { 'User-Agent': 'Node.js' } }, (response) => { + // Handle redirects + if (response.statusCode === 302 || response.statusCode === 301) { + file.destroy(); + fs.unlink(dest, () => {}); + return downloadFile(response.headers.location, dest) + .then(resolve) + .catch(reject); + } + + // Handle 404 + if (response.statusCode === 404) { + file.destroy(); + fs.unlink(dest, () => {}); + reject(new Error( + `Release artifact not found (404). Check:\n` + + ` - Repository exists: ${url.split('/releases')[0]}\n` + + ` - Release tag exists: ${RELEASE_TAG}\n` + + ` - Artifact filename is correct` + )); + return; + } + + // Handle 403 + if (response.statusCode === 403) { + file.destroy(); + fs.unlink(dest, () => {}); + reject(new Error( + `Access denied (403). The repository may be private.\n` + + `Set GITHUB_TOKEN environment variable for private repos.` + )); + return; + } + + // Handle other HTTP errors + if (response.statusCode < 200 || response.statusCode >= 300) { + file.destroy(); + fs.unlink(dest, () => {}); + reject(new Error( + `HTTP ${response.statusCode}: Failed to download from ${url}` + )); + return; + } + + response.pipe(file); + + file.on('finish', () => { + file.close(); + resolve(); + }); + + file.on('error', (err) => { + fs.unlink(dest, () => {}); + reject(new Error(`File write error: ${err.message}`)); + }); + }).on('error', (err) => { + file.destroy(); + fs.unlink(dest, () => {}); + reject(new Error(`Download error: ${err.message}`)); + }); + }); +} + +install(); diff --git a/packages/simplex-chat-nodejs/src/index.ts b/packages/simplex-chat-nodejs/src/index.ts new file mode 100644 index 0000000000..80180ae282 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/index.ts @@ -0,0 +1,22 @@ +/** + * A simple declarative API to run a chat-bot with a single function call. + * It automates creating and updating of the bot profile, address and bot commands shown in the app UI. + */ +export * as bot from "./bot" + +/** + * An API to send chat commands and receive chat events to/from chat core. + * You need to use it in bot event handlers, and for any other use cases. + */ +export * as api from "./api" + +/** + * A low level API to the core library - the same that is used in desktop clients. + * You are unlikely to ever need to use this module directly. + */ +export * as core from "./core" + +/** + * Useful functions for chat events and types. + */ +export * as util from "./util" diff --git a/packages/simplex-chat-nodejs/src/simplex.d.ts b/packages/simplex-chat-nodejs/src/simplex.d.ts new file mode 100644 index 0000000000..10c2f6608a --- /dev/null +++ b/packages/simplex-chat-nodejs/src/simplex.d.ts @@ -0,0 +1,10 @@ +// These functions are defined in CPP add-on ../cpp/simplex.cc + +export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]> +export function chat_close_store(ctrl: bigint): Promise +export function chat_send_cmd(ctrl: bigint, cmd: string): Promise +export function chat_recv_msg_wait(ctrl: bigint, wait: number): Promise +export function chat_write_file(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise +export function chat_read_file(path: string, key: string, nonce: string): Promise +export function chat_encrypt_file(ctrl: bigint, fromPath: string, toPath: string): Promise +export function chat_decrypt_file(fromPath: string, key: string, nonce: string, toPath: string): Promise diff --git a/packages/simplex-chat-nodejs/src/simplex.js b/packages/simplex-chat-nodejs/src/simplex.js new file mode 100644 index 0000000000..fd26c67438 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/simplex.js @@ -0,0 +1 @@ +module.exports = require("../build/Release/simplex") diff --git a/packages/simplex-chat-nodejs/src/util.ts b/packages/simplex-chat-nodejs/src/util.ts new file mode 100644 index 0000000000..52e7843a95 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/util.ts @@ -0,0 +1,92 @@ +import {T} from "@simplex-chat/types" +import {BotAddressSettings} from "./api" + +export function chatInfoRef(cInfo: T.ChatInfo): T.ChatRef | undefined { + switch (cInfo.type) { + case T.ChatType.Direct: return {chatType: T.ChatType.Direct, chatId: cInfo.contact.contactId} + case T.ChatType.Group: { + const chatScope: T.GroupChatScope | undefined = + cInfo.groupChatScope?.type == "memberSupport" + ? {type: "memberSupport", groupMemberId_: cInfo.groupChatScope.groupMember_?.groupMemberId} + : undefined + return {chatType: T.ChatType.Group, chatId: cInfo.groupInfo.groupId, chatScope} + } + default: return undefined + } +} + +export function chatInfoName(cInfo: T.ChatInfo): string { + switch (cInfo.type) { + case "direct": return `@${cInfo.contact.profile.displayName}` + case "group": { + const scope = cInfo.groupChatScope + const scopeName = scope?.type === "memberSupport" + ? `(support${scope.groupMember_ ? ` ${scope.groupMember_.memberProfile.displayName}` : ""})` + : "" + return `#${cInfo.groupInfo.groupProfile.displayName}${scopeName}` + } + case "local": return "private notes" + case "contactRequest": return `request from @${cInfo.contactRequest.profile.displayName}` + case "contactConnection": { + const alias = cInfo.contactConnection.localAlias + return `pending connection${alias ? ` (@${alias})` : ""}` + } + } +} + +export function senderName(cInfo: T.ChatInfo, chatDir: T.CIDirection) { + const sender = chatDir.type === "groupRcv" + ? ` @${chatDir.groupMember.memberProfile.displayName}` + : "" + return chatInfoName(cInfo) + sender +} + +export function contactAddressStr(link: T.CreatedConnLink): string { + return link.connShortLink || link.connFullLink +} + +export function botAddressSettings({addressSettings}: T.UserContactLink): BotAddressSettings { + return { + autoAccept: addressSettings.autoAccept ? true : false, + welcomeMessage: addressSettings.autoReply, + businessAddress: addressSettings.businessAddress + } +} + +export function fromLocalProfile({displayName, fullName, shortDescr, image, contactLink, preferences, peerType}: T.LocalProfile): T.Profile { + const profile = {displayName, fullName, shortDescr, image, contactLink, preferences, peerType} + for (const key in profile) { + if (typeof (profile as any)[key] === "undefined") delete (profile as any)[key] + } + return profile +} + +export function ciContentText({content}: T.ChatItem): string | undefined { + switch (content.type) { + case "sndMsgContent": return content.msgContent.text; + case "rcvMsgContent": return content.msgContent.text; + default: return undefined; + } +} + +export interface BotCommand { + keyword: string + params: string +} + +// returns command (without /) and trimmed parameters +export function ciBotCommand(chatItem: T.ChatItem): BotCommand | undefined { + const msg = ciContentText(chatItem)?.trim() + if (msg) { + const r = msg.match(/\/([^\s]+)(.*)/) + if (r && r.length >= 3) { + return {keyword: r[1], params: r[2].trim()} + } + } + return undefined +} + +export function reactionText(reaction: T.ACIReaction): string { + const r = reaction.chatReaction + return r.reaction.type === "emoji" ? r.reaction.emoji : r.reaction.tag +} diff --git a/packages/simplex-chat-nodejs/tests/api.test.ts b/packages/simplex-chat-nodejs/tests/api.test.ts new file mode 100644 index 0000000000..52153ecfed --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/api.test.ts @@ -0,0 +1,67 @@ +import * as path from "path" +import * as fs from "fs" +import {CEvt, T} from "@simplex-chat/types" +import {api} from ".." + +const CT = T.ChatType + +describe("API tests (use preset servers)", () => { + const tmpDir = "./tests/tmp" + const alicePath = path.join(tmpDir, "alice") + const bobPath = path.join(tmpDir, "bob") + + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})) + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})) + + it("should send/receive message", async () => { + // create users and start chat controllers + const alice = await api.ChatApi.init(alicePath) + const bob = await api.ChatApi.init(bobPath) + const servers: string[] = [] + let eventCount = 0 + alice.on("hostConnected" as CEvt.Tag, async ({transportHost}: any) => { servers.push(transportHost) }) + alice.onAny(async () => { eventCount++ }) + await expect(alice.apiGetActiveUser()).resolves.toBeUndefined() + const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) + await expect(alice.apiGetActiveUser()).resolves.toMatchObject(aliceUser) + await bob.apiCreateActiveUser({displayName: "bob", fullName: ""}) + await alice.startChat() + await bob.startChat() + // connect via link + const link = await alice.apiCreateLink(aliceUser.userId) + await expect(bob.apiConnectActiveUser(link)).resolves.toBe(api.ConnReqType.Invitation) + const [bobContact, aliceContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await bob.wait("contactConnected")).contact + ]) + expect(bobContact).toMatchObject({profile: {displayName: "bob"}}) + expect(aliceContact).toMatchObject({profile: {displayName: "alice"}}) + // exchange messages + const isMessage = ({contactId}: T.Contact, msg: string) => (evt: CEvt.NewChatItems) => + evt.chatItems.some(ci => ci.chatInfo.type === CT.Direct && ci.chatInfo.contact.contactId === contactId && ci.chatItem.meta.itemText === msg) + await alice.apiSendTextMessage([CT.Direct, bobContact.contactId], "hello") + await bob.wait("newChatItems", isMessage(aliceContact, "hello")) + await bob.apiSendTextMessage([CT.Direct, aliceContact.contactId], "hello too") + await alice.wait("newChatItems", isMessage(bobContact, "hello too"), 10000) + await alice.apiSendTextMessage([CT.Direct, bobContact.contactId], "how are you?") + await bob.wait("newChatItems", isMessage(aliceContact, "how are you?")) + await bob.apiSendTextMessage([CT.Direct, aliceContact.contactId], "ok, and you?") + await alice.wait("newChatItems", isMessage(bobContact, "ok, and you?"), 10000) + // no more messages + await expect(alice.wait("newChatItems", 500)).resolves.toBeUndefined() + await expect(bob.wait("newChatItems", 500)).resolves.toBeUndefined() + // delete contacts, stop chat controllers and close databases + await alice.apiDeleteChat(CT.Direct, bobContact.contactId) + await bob.wait("contactDeletedByContact") + await bob.apiDeleteChat(CT.Direct, aliceContact.contactId) + await alice.stopChat() + await bob.stopChat() + await alice.close() + await bob.close() + await expect(alice.startChat).rejects.toThrow() + await expect(bob.startChat).rejects.toThrow() + expect(servers.length).toBe(2) + expect(servers[0] !== servers[1]).toBe(true) + expect(eventCount > 0).toBe(true) + }, 30000) +}) diff --git a/packages/simplex-chat-nodejs/tests/bot.test.ts b/packages/simplex-chat-nodejs/tests/bot.test.ts new file mode 100644 index 0000000000..b1fd9d0186 --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/bot.test.ts @@ -0,0 +1,60 @@ +import * as path from "path" +import * as fs from "fs" +import * as assert from "assert" +import {CEvt, T} from "@simplex-chat/types" +import {api, bot, util} from ".." + +const CT = T.ChatType + +describe("Bot tests (use preset servers)", () => { + const tmpDir = "./tests/tmp" + const botPath = path.join(tmpDir, "bot") + const alicePath = path.join(tmpDir, "alice") + + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})) + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})) + + it("should reply to messages", async () => { + // run bot + const [chat, botUser, botAddress] = await bot.run({ + profile: {displayName: "Squaring bot", fullName: ""}, + dbOpts: {dbFilePrefix: botPath, dbKey: ""}, + options: { + addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."}, + }, + onMessage: async (ci, content) => { + const n = +content.text + const reply = typeof n === "number" && !isNaN(n) ? `${n} * ${n} = ${n * n}` : `this is not a number` + await chat.apiSendTextReply(ci, reply) + } + }) + assert(typeof botAddress === "object") + // create user + const alice = await api.ChatApi.init(alicePath) + const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) + await alice.startChat() + // connect to bot + const [plan, link] = await alice.apiConnectPlan(aliceUser.userId, util.contactAddressStr(botAddress.connLinkContact)) + assert(plan.type === "contactAddress") + await expect(alice.apiConnect(aliceUser.userId, false, link)).resolves.toBe(api.ConnReqType.Contact) + const [botContact, aliceContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await chat.wait("contactConnected")).contact + ]) + expect(botContact.profile.displayName).toBe("Squaring bot") + // send message to bot + const isMessage = ({contactId}: T.Contact, msg: string) => (evt: CEvt.NewChatItems) => + evt.chatItems.some(ci => ci.chatInfo.type === CT.Direct && ci.chatInfo.contact.contactId === contactId && ci.chatItem.meta.itemText === msg) + await alice.apiSendTextMessage([CT.Direct, botContact.contactId], "2") + console.log("after sending message") + await alice.wait("newChatItems", isMessage(botContact, "2 * 2 = 4"), 5000) + // cleanup + await alice.apiDeleteChat(CT.Direct, botContact.contactId) + await chat.wait("contactDeletedByContact", ({contact}) => contact.contactId === aliceContact.contactId) + await chat.apiDeleteUserAddress(botUser.userId) + await chat.stopChat() + await chat.close() + await alice.stopChat() + await alice.close() + }, 30000) +}) diff --git a/packages/simplex-chat-nodejs/tests/core.test.ts b/packages/simplex-chat-nodejs/tests/core.test.ts new file mode 100644 index 0000000000..141f35746d --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/core.test.ts @@ -0,0 +1,85 @@ +import * as fs from "fs"; +import * as path from "path"; +import {core} from "../src/index"; + +describe("Core tests", () => { + const tmpDir = "./tests/tmp"; + const dbPath = path.join(tmpDir, "simplex_v1"); + + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})); + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})); + + it("should initialize chat controller", async () => { + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); + expect(typeof ctrl).toBe("bigint"); + await expect(core.chatCloseStore(ctrl)).resolves.toBe(undefined); + + await expect(core.chatMigrateInit(dbPath, "wrong_key", core.MigrationConfirmation.YesUp)).rejects.toMatchObject({ + message: "Database or migration error (see dbMigrationError property)", + dbMigrationError: expect.objectContaining({type: "errorNotADatabase"}) + }); + }); + + it("should send command and receive event", async () => { + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); + + await expect(core.chatSendCmd(ctrl, "/v")).resolves.toMatchObject({ + type: "versionInfo" + }); + await expect(core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}')).resolves.toMatchObject({ + type: "cmdOk" + }); + + const wait = 500_000; + await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toMatchObject({ + type: "chatSuspended" + }); + await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toBe(undefined); + + await expect(core.chatSendCmd(ctrl, "/unknown")).rejects.toMatchObject({ + message: "Chat command error (see chatError property)", + chatError: expect.objectContaining({type: "error"}) + }); + + await core.chatCloseStore(ctrl); + }); + + it("should write/read encrypted file from/to buffer", async () => { + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); + + const filePath = path.join(tmpDir, "write_file.txt"); + const buffer = new Uint8Array([0, 1, 2]).buffer; + const cryptoArgs = await core.chatWriteFile(ctrl, filePath, buffer); + expect(typeof cryptoArgs.fileKey).toBe("string"); + expect(typeof cryptoArgs.fileNonce).toBe("string"); + + const buffer2 = await core.chatReadFile(filePath, cryptoArgs); + expect(Buffer.from(buffer2).equals(Buffer.from(buffer))).toBe(true); + + await expect(core.chatWriteFile(ctrl, path.join(tmpDir, "unknown", "unknown.txt"), buffer)).rejects.toThrow(); + await expect(core.chatReadFile(path.join(tmpDir, "unknown.txt"), cryptoArgs)).rejects.toThrow(); + + await core.chatCloseStore(ctrl); + }); + + it("should encrypt/decrypt file", async () => { + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); + + const unencryptedPath = path.join(tmpDir, "file_unencrypted.txt"); + fs.writeFileSync(unencryptedPath, "unencrypted\n"); + const encryptedPath = path.join(tmpDir, "file_encrypted.txt"); + const cryptoArgs = await core.chatEncryptFile(ctrl, unencryptedPath, encryptedPath); + expect(typeof cryptoArgs.fileKey).toBe("string"); + expect(typeof cryptoArgs.fileNonce).toBe("string"); + + const decryptedPath: string = path.join(tmpDir, "file_decrypted.txt"); + await expect(core.chatDecryptFile(encryptedPath, cryptoArgs, decryptedPath)).resolves.toBe(undefined); + + expect(fs.readFileSync(decryptedPath, "utf8")).toBe("unencrypted\n"); + + await expect(core.chatEncryptFile(ctrl, path.join(tmpDir, "unknown.txt"), encryptedPath)).rejects.toThrow(); + await expect(core.chatDecryptFile(path.join(tmpDir, "unknown.txt"), cryptoArgs, decryptedPath)).rejects.toThrow(); + + await core.chatCloseStore(ctrl); + }); +}); diff --git a/packages/simplex-chat-nodejs/tests/tsconfig.json b/packages/simplex-chat-nodejs/tests/tsconfig.json new file mode 100644 index 0000000000..47e973f3af --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": ["**/*", "../src/**/*"] +} diff --git a/packages/simplex-chat-nodejs/tsconfig.json b/packages/simplex-chat-nodejs/tsconfig.json new file mode 100644 index 0000000000..f5dc986431 --- /dev/null +++ b/packages/simplex-chat-nodejs/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["src"], + "compilerOptions": { + "declaration": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2018"], + "module": "CommonJS", + "moduleResolution": "Node", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noEmitOnError": true, + "outDir": "dist", + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "target": "ES2018", + "types": ["node"] + } +} diff --git a/packages/simplex-chat-nodejs/typedoc.json b/packages/simplex-chat-nodejs/typedoc.json new file mode 100644 index 0000000000..3523115f49 --- /dev/null +++ b/packages/simplex-chat-nodejs/typedoc.json @@ -0,0 +1,14 @@ +{ + "name": "simplex-chat", + "plugin": ["typedoc-plugin-markdown"], + "entryPoints": [ + "./src/index.ts", + "../simplex-chat-client/types/typescript/src/index.ts" + ], + "entryPointStrategy": "expand", + "tsconfig": "./tsconfig.json", + "sourceLinkTemplate": "../{path}#L{line}", + "disableGit": true, + "flattenOutputFiles": true, + "out": "./docs" +} \ No newline at end of file diff --git a/plans/2026-03-05-members-conn-errors.md b/plans/2026-03-05-members-conn-errors.md new file mode 100644 index 0000000000..b63923771a --- /dev/null +++ b/plans/2026-03-05-members-conn-errors.md @@ -0,0 +1,316 @@ +# Save Permanent Connection Errors for Group Members + +## Context + +When a group member's connection handshake fails with a permanent error (e.g., `CONN NOT_ACCEPTED`, `SMP AUTH`, `AGENT A_VERSION`), the ERR event is logged to the UI event stream and discarded. The member record stays stuck in a "connecting" `GroupMemberStatus` (like `memIntroduced`, `memAccepted`) forever. Users see perpetual "connecting" with no explanation and no way to know whether to wait or re-invite. + +**Root cause**: `agentMsgConnStatus` (Subscriber.hs:376) only maps success events (`CONF`, `INFO`, `JOINED`, `CON`) to status transitions. The ERR handler for group members (Subscriber.hs:1054-1056) only logs to UI and completes the command — no status or error is persisted. + +## Solution Summary + +Add `ConnError {connError :: Text}` constructor to `ConnStatus`. Error text is encoded in the `conn_status TEXT` column as `"error "` via `TextEncoding`, and in JSON via `sumTypeJSON` (following `GSSError`/`CIFileStatus` pattern). No new DB column, no migration. When a non-temporary ERR arrives before connection is ready, transition to `ConnError` and notify UI. Messages are not queued for errored connections. + +## Technical Design + +### Error classification + +Use `temporaryOrHostError` from `Simplex.Messaging.Agent.Client` (simplexmq Client.hs:1486, exported at line 60): +- Returns `True` for NETWORK, TIMEOUT, HOST, TEVersion, INACTIVE, CRITICAL-with-restart → **do not save** +- Returns `False` for AUTH, CONN errors, VERSION, INTERNAL, etc. → **save as permanent error** + +Guard: only save when connection is not `ConnReady` and not already `ConnError`. Post-handshake errors (when `connStatus == ConnReady`) are handled by existing `processConnMERR` (AUTH counters, QUOTA counters). + +### Data flow + +``` +Agent ERR event + → Subscriber.hs processGroupMessage ERR handler + → guard: connStatus is not ConnReady, not ConnError, not temporaryOrHostError + → DB: UPDATE connections SET conn_status = 'error ' + → emit: CEvtGroupMemberUpdated user gInfo m m' + → iOS: upsertGroupMember updates model → UI re-renders +``` + +### DB encoding + +`conn_status TEXT NOT NULL` already exists. `ConnError` encodes as `"error " <> errText` using `TextEncoding` (same as `GSSError`). No migration needed — new text values are valid in the existing column. + +### JSON encoding + +Replace manual `ToJSON`/`FromJSON` instances with `$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Conn") ''ConnStatus)`. This follows the `GroupSndStatus`/`CIFileStatus` pattern — `sumTypeJSON` is already imported in Types.hs (line 60). + +JSON format (platform-dependent via `sumTypeJSON`): +- iOS: `{"error": {"connError": "SMP AUTH"}}` (ObjectWithSingleField) +- Android/Desktop: `{"type": "error", "connError": "SMP AUTH"}` (TaggedObject) +- Nullary cases: `{"ready": {}}` / `{"type": "ready"}` (not plain `"ready"` strings) + +Note: `ConnSndReady` JSON tag changes from `"snd-ready"` to `"sndReady"` (`dropPrefix "Conn"` applies `fstToLower`). This is safe — JSON is core→UI within same build. Swift auto-synthesis matches on `sndReady` case name. + +### Clear on recovery + +When CON event arrives, `agentMsgConnStatus` returns `Just ConnReady`, `updateConnStatus` overwrites `conn_status` to `"ready"`. Error is implicitly cleared — no special cleanup needed. + +### ConnStatus state machine update + +``` +Existing transitions (unchanged): + ConnNew → ConnRequested → ConnAccepted → ConnSndReady → ConnReady + ConnNew → ConnJoined → ConnSndReady → ConnReady + ConnPrepared → ConnJoined → ConnSndReady → ConnReady + Any → ConnDeleted + +New transitions: + Any pre-ready state → ConnError (on permanent ERR) + ConnError → ConnReady (on successful CON — recovery) + ConnError → ConnDeleted (on connection deletion) +``` + +### Pattern match safety audit + +Traced every ConnStatus pattern match across Haskell (10 files), Swift (6 files), Kotlin (3 files). + +**Must update (exhaustive matches):** + +| Location | Change | +|---|---| +| Types.hs textEncode/textDecode (~1703) | Add ConnError encoding/decoding | +| Types.hs ToJSON/FromJSON (~1696) | Replace with `sumTypeJSON` TH splice | +| Swift ConnStatus.initiated | Add `case .error: return nil` | +| Kotlin ConnStatus.initiated | Add `Error -> null` (follow-up) | + +**Must update (behavioral):** + +| Location | Current behavior | Fix | +|---|---|---| +| Internal.hs memberSendAction (line 2041) | ConnError falls to `otherwise -> pendingOrForwarded` — messages queued for permanently errored connections | Add pattern guard `ConnError {} <- connStatus -> Nothing` | + +**Verified safe — no changes needed:** + +| Pattern | Sites | Why safe | +|---|---|---| +| `== ConnReady` / `== ConnSndReady` | 12 sites (connReady, Contact.ready, GroupMember.ready, sndReady, readyMemberConn, xftpSndFileTransfer) | ConnError ≠ these → excluded from "ready" paths | +| `== ConnPrepared` | 8 sites (joinPreparedConn, nextConnectPrepared, isContactCard, contactRequestPlan) | ConnError ≠ ConnPrepared → doesn't trigger join/prepare logic | +| `== ConnNew` | 4 sites (contactConnInitiated, nextAcceptContactRequest, APIPrepareContact) | ConnError ≠ ConnNew → doesn't trigger new-connection logic | +| `!= ConnDeleted` (DB WHERE) | 6 sites (getConnectionEntity, *ConnsToSub) | ConnError ≠ ConnDeleted → errored connections remain findable and subscribable (correct — enables recovery via CON). **Add TODO comments** at each site to consider whether ConnError connections should be excluded. | +| `updateConnectionStatusFromTo` | 3 sites | Compares current to specific `fromStatus` — ConnError won't accidentally match | +| `readyMemberConn` (Internal.hs:2078) | 1 site | `connStatus == ConnReady \|\| == ConnSndReady` — ConnError → `otherwise = Nothing` (correct) | +| `connDisabled`/`connInactive` | 6 sites | Derived from error counters, not connStatus | +| `agentMsgConnStatus` | 1 site | Only produces ConnSndReady/ConnRequested/ConnReady — no ConnError output | + +## Implementation Plan + +### 1. Haskell: ConnStatus type + +**File: `src/Simplex/Chat/Types.hs`** + +**ConnStatus** (~line 1673): Add constructor after `ConnDeleted`: +```haskell + | ConnError {connError :: Text} +``` +Record syntax for `sumTypeJSON` field name in JSON. `deriving (Eq, Show, Read)` unchanged. + +**TextEncoding instance** (~line 1703) — for DB storage: +```haskell + textEncode = \case + ... + ConnError err -> "error " <> err + textDecode s + | Just err <- T.stripPrefix "error " s = Just (ConnError err) + | otherwise = case s of + "new" -> Just ConnNew + ... (existing cases unchanged) + _ -> Nothing +``` + +Note: `textDecode` changes from `\case` to named parameter `s` to support `stripPrefix` guard. + +**JSON instances** (~lines 1696-1701): Replace manual instances with TH splice: +```haskell +-- Remove: +-- instance FromJSON ConnStatus where parseJSON = textParseJSON "ConnStatus" +-- instance ToJSON ConnStatus where toJSON = J.String . textEncode; toEncoding = JE.text . textEncode +-- Add: +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Conn") ''ConnStatus) +``` + +`sumTypeJSON` and `dropPrefix` already imported (line 60). `FromField`/`ToField` instances unchanged — still use `TextEncoding` for DB. + +**`connReady`** (line 1597): No change — `== ConnReady || == ConnSndReady`, `ConnError _` naturally returns `False`. + +### 2. Haskell: Subscriber.hs — save error on permanent ERR + +**File: `src/Simplex/Chat/Library/Subscriber.hs`** + +Extend existing import (line 74): +```haskell +import Simplex.Messaging.Agent.Client (temporaryOrHostError, getAgentWorker, ...) +``` + +Update ERR handler in `processGroupMessage` (line 1054-1056). Current: +```haskell +ERR err -> do + eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () +``` + +New: +```haskell +ERR err -> do + eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + let Connection {connStatus = cs} = conn + case cs of + ConnReady -> pure () + ConnError _ -> pure () + _ | temporaryOrHostError err -> pure () + | otherwise -> do + let errText = tshow err + withStore' $ \db -> updateConnectionStatus db conn (ConnError errText) + let conn' = conn {connStatus = ConnError errText} + m' = m {activeConn = Just conn'} + toView $ CEvtGroupMemberUpdated user gInfo m m' +``` + +Note: `let Connection {connStatus = cs} = conn` destructures via pattern binding, avoiding ambiguous `connStatus conn` field selector under `DuplicateRecordFields`. + +No new store function — reuses existing `updateConnectionStatus` (Direct.hs:937) which calls `updateConnectionStatus_` → `textEncode` → stores `"error SMP AUTH"` in `conn_status`. + +### 3. Haskell: memberSendAction — don't queue for errored connections + +**File: `src/Simplex/Chat/Library/Internal.hs`** + +Update `memberSendAction` (line 2040-2044). Current: +```haskell + Just conn@Connection {connStatus} + | connDisabled conn || connStatus == ConnDeleted || memberStatus == GSMemRejected -> Nothing + | connInactive conn -> Just MSAPending + | connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn + | otherwise -> pendingOrForwarded +``` + +Add pattern guard after first guard (can't use `==` with associated data): +```haskell + Just conn@Connection {connStatus} + | connDisabled conn || connStatus == ConnDeleted || memberStatus == GSMemRejected -> Nothing + | ConnError {} <- connStatus -> Nothing + | connInactive conn -> Just MSAPending + | connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn + | otherwise -> pendingOrForwarded +``` + +### 4. Swift: ConnStatus enum + +**File: `apps/ios/SimpleXChat/ChatTypes.swift`** (~line 2301) + +Change from `String`-backed raw value enum to enum with associated value. Auto-synthesized `Decodable` handles `sumTypeJSON` format (same as `GroupSndStatus`, `CIFileStatus`): + +```swift +public enum ConnStatus: Decodable, Hashable { + case new + case prepared + case joined + case requested + case accepted + case sndReady + case ready + case deleted + case error(connError: String) + + var initiated: Bool? { + switch self { + case .new: return true + case .prepared: return false + case .joined: return false + case .requested: return true + case .accepted: return true + case .sndReady: return nil + case .ready: return nil + case .deleted: return nil + case .error: return nil + } + } +} +``` + +No custom `init(from:)` needed. `Hashable`/`Equatable` auto-synthesized. Existing equality checks like `connStatus == .ready` still compile (nullary cases). + +### 5. Swift: Connection computed property + +**File: `apps/ios/SimpleXChat/ChatTypes.swift`** + +Add computed property to `Connection` struct (~line 2092, after `connStatus`): +```swift +public var connError: String? { + if case let .error(err) = connStatus { return err } + return nil +} +``` + +### 6. Swift: Member list status + +**File: `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift`** + +Update `memberConnStatus` function (~line 457). Insert error check FIRST (before `connDisabled`/`connInactive`): +```swift +private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey { + if case .error = member.activeConn?.connStatus { + return "connection error" + } else if member.activeConn?.connDisabled ?? false { + return "disabled" + } else if member.activeConn?.connInactive ?? false { + return "inactive" + } else { + return member.memberStatus.shortText + } +} +``` + +**File: `apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift`** + +Update `memberStatus` function (line 198). Insert error check FIRST (before `connDisabled` at line 199): +```swift + if case .error = member.activeConn?.connStatus { + return "connection error" + } else if member.activeConn?.connDisabled ?? false { +``` + +### 7. Swift: Member info error display + +**File: `apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift`** + +Add error display section after the `connStats` section (~line 190): +```swift +if let connError = member.activeConn?.connError { + Section(header: Text("Connection error").foregroundColor(theme.colors.secondary)) { + Text(connError) + .foregroundColor(theme.colors.secondary) + .font(.callout) + .textSelection(.enabled) + } +} +``` + +## Files Changed Summary + +| Layer | File | Change | +|-------|------|--------| +| Core | `Types.hs` | Add `ConnError {connError :: Text}` to ConnStatus, update TextEncoding, replace JSON with `sumTypeJSON` TH splice | +| Logic | `Subscriber.hs` | Import `temporaryOrHostError`, handle permanent ERR for group members | +| Logic | `Internal.hs` | Add `ConnError` guard to `memberSendAction` → return `Nothing` | +| iOS | `ChatTypes.swift` | ConnStatus: auto-synthesized Decodable with `.error(connError:)`, Connection: `connError` computed property | +| iOS | `GroupChatInfoView.swift` | Show "connection error" in `memberConnStatus` (first check) | +| iOS | `MemberSupportView.swift` | Show "connection error" in `memberStatus` (first check) | +| iOS | `GroupMemberInfoView.swift` | Show error description section | + +## Verification + +1. **Build Haskell**: `cabal build --ghc-options -O0` +2. **Build iOS**: Verify Swift compiles — existing `connStatus == .ready` comparisons still work (nullary cases) +3. **JSON format**: Verify `sumTypeJSON` output matches Swift auto-synthesis expectations (nullary: `{"ready": {}}`, error: `{"error": {"connError": "..."}}`) +4. **Backward compat**: New `"error ..."` values in `conn_status` only appear after code update. Old code cannot parse them (downgrade risk, same as any new enum value). +5. **Recovery**: CON event → `updateConnectionStatus_ ConnReady` → overwrites `"error ..."` with `"ready"` in DB +6. **memberSendAction**: Verify messages are NOT queued for ConnError connections + +## Out of Scope (immediate follow-up) + +**Kotlin/Android/Desktop**: `ConnStatus` enum in `ChatModel.kt:2640` needs custom serializer for `sumTypeJSON` format (TaggedObject: `{"type": "error", "connError": "..."}`) + `Connection` needs `connError` computed property + member status UI. Must be updated before Android/Desktop builds from this commit. Existing bug at `GroupChatInfoView.kt:883` (`connDisabled` checked twice, should be `connInactive` on second check). diff --git a/plans/2026-03-21-text-size-markdown.md b/plans/2026-03-21-text-size-markdown.md new file mode 100644 index 0000000000..15389a2c1b --- /dev/null +++ b/plans/2026-03-21-text-size-markdown.md @@ -0,0 +1,66 @@ +# Small Text Markdown + +Add `!- text!` syntax for small gray text — legal disclaimers, secondary commentary, LLM reasoning, etc. + +## Syntax + +`!- text!` — renders as small gray text. Uses the `!` style prefix family, `-` for "reduced." + +On old clients: `!- fine print!` shows as-is (old `coloredP` fails on `-`, falls to `wordP`). Readable. + +## Changes + +### Haskell — `src/Simplex/Chat/Markdown.hs` + +1. **`Format`**: add `Small` constructor (no fields). + +2. **`coloredP` parser**: before trying `colorP`, check for `-` followed by space. If matched, produce `Small`. Otherwise fall through to existing color parsing. + +3. **`markdownText`**: add `Small` case, reconstruct as `!- text!`. + +4. **JSON serialization**: TH-derived `ToJSON`/`FromJSON` via existing `sumTypeJSON fstToLower`. Produces `{"small": {}}`. Old Haskell `FromJSON Format` falls to `Unknown` via `<|> pure (Unknown v)`. + +### Haskell — `src/Simplex/Chat/Styled.hs` + +5. **`sgr`**: add `Small` case — map to `FaintIntensity` for terminal rendering. + +### Haskell — `tests/MarkdownTests.hs` + +6. Tests for: + - `!- text!` parses as `Small` + - `!- text!` with leading/trailing spaces in content → no format (same rule as other formats) + - Existing color syntax unchanged + - `markdownText` round-trip + +### iOS — `apps/ios/SimpleXChat/ChatTypes.swift` + +7. **`Format`** enum: add `case small`. + +### iOS — `apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift` + +8. **`messageText`**: render `Small` with smaller `UIFont` point size + gray color. + +### Android — `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` + +9. **`Format`**: add `@Serializable @SerialName("small") class Small: Format()`. +10. **`Format.style`**: `SpanStyle` with smaller font size + gray color. + +### Android — `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt` + +11. **`MarkdownText`**: add `is Format.Small` case — same pattern as `Bold`/`Italic` (apply style, append text). + +## Backward Compatibility + +### Local (old app receiving message with new syntax) +- Old app's bundled Haskell parses raw message text. Old `coloredP` doesn't know `-`, fails, falls to `wordP`. Text shows as `!- fine print!` — plain text with delimiters. + +### Remote desktop (old desktop, new mobile) +- New mobile Haskell parses `!- text!` as `Small`, serializes to JSON `{"small": {}}`. +- Old desktop Haskell re-parses JSON via `J.parseJSON` (`Remote/Protocol.hs:184`). Old `FromJSON Format` doesn't know `"small"` → `<|> pure (Unknown v)`. +- `Unknown` re-serializes to `{"type": "unknown", "json": ...}` → Kotlin `Format.Unknown` (`ignoreUnknownKeys` drops extra fields). Text renders without formatting. + +## Order of Implementation + +1. Haskell types + parser + tests +2. iOS types + rendering +3. Android types + rendering diff --git a/plans/audio-captcha-improvements.md b/plans/audio-captcha-improvements.md new file mode 100644 index 0000000000..6797115396 --- /dev/null +++ b/plans/audio-captcha-improvements.md @@ -0,0 +1,520 @@ +# Audio Captcha Improvements Plan + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [High-Level Design](#high-level-design) +3. [Detailed Implementation Plan](#detailed-implementation-plan) +4. [Test Updates](#test-updates) +5. [Files Changed](#files-changed) + +--- + +## Executive Summary + +Improve the audio captcha feature by: + +1. **Proper command parsing** — add `DCCaptchaMode CaptchaMode` constructor to `DirectoryCmd` GADT, using existing Attoparsec parsing infrastructure +2. **Audio captcha retry** — when user switches to audio mode, subsequent retries send voice captcha (not image) +3. **Make `/audio` clickable** — use `/'audio'` format for clickable command in chat UI + +--- + +## High-Level Design + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ CaptchaMode (Events.hs) │ +├──────────────────────────────────────────────────────────────────┤ +│ CMText -- default image/text captcha │ +│ CMAudio -- voice captcha mode │ +└──────────────────────────────────────────────────────────────────┘ + +┌──────────────────────────────────────────────────────────────────┐ +│ PendingCaptcha State │ +├──────────────────────────────────────────────────────────────────┤ +│ captchaText :: Text -- the captcha answer │ +│ sentAt :: UTCTime -- when captcha was sent │ +│ attempts :: Int -- number of attempts │ +│ captchaMode :: CaptchaMode -- current mode (CMText/CMAudio) │ +└──────────────────────────────────────────────────────────────────┘ + +┌──────────────────────────────────────────────────────────────────┐ +│ DirectoryCmd (Events.hs) │ +├──────────────────────────────────────────────────────────────────┤ +│ DCCaptchaMode :: CaptchaMode -> DirectoryCmd 'DRUser │ +│ (integrated into existing GADT, parsed via directoryCmdP) │ +└──────────────────────────────────────────────────────────────────┘ + +Flow: +1. User joins group → sendMemberCaptcha (image) + captchaNotice with /'audio' +2. User sends /audio → parsed as DCCaptchaMode CMAudio → set captchaMode=CMAudio, sendVoiceCaptcha +3. User sends wrong answer: + - captchaMode=CMText → send new IMAGE captcha + - captchaMode=CMAudio → send new VOICE captcha ← NEW BEHAVIOR +4. User sends correct answer → approve member + +Message parsing flow (in Service.hs dePendingMemberMsg): +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Parse msgText with directoryCmdP (existing infrastructure) │ +│ ↓ │ +│ 2. TM.lookup pendingCaptcha (ONCE, not per-branch) │ +│ ↓ │ +│ ├─ Nothing → sendMemberCaptcha with mode from parsed cmd │ +│ └─ Just pc → case on parsed cmd: │ +│ ├─ DCCaptchaMode CMAudio → set mode, send voice captcha │ +│ ├─ DCSearchGroup _ → captcha answer (verify/retry) │ +│ └─ _ → unknown command (error message) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Detailed Implementation Plan + +### 3.1 Add `CaptchaMode` type in Events.hs + +**File:** `apps/simplex-directory-service/src/Directory/Events.hs` + +**Location:** After `DirectoryHelpSection` (line 146) + +**Add:** +```haskell +data CaptchaMode = CMText | CMAudio + deriving (Show) +``` + +**Update exports (line 10-19):** +```haskell +module Directory.Events + ( DirectoryEvent (..), + DirectoryCmd (..), + ADirectoryCmd (..), + DirectoryHelpSection (..), + CaptchaMode (..), + DirectoryRole (..), + SDirectoryRole (..), + crDirectoryEvent, + directoryCmdP, + directoryCmdTag, + ) +where +``` + +--- + +### 3.2 Add `DCCaptchaMode_` tag in Events.hs + +**File:** `apps/simplex-directory-service/src/Directory/Events.hs` + +**Location:** In `DirectoryCmdTag` GADT (after line 127, before admin commands) + +**Add:** +```haskell + DCCaptchaMode_ :: DirectoryCmdTag 'DRUser +``` + +--- + +### 3.3 Add `DCCaptchaMode` constructor in Events.hs + +**File:** `apps/simplex-directory-service/src/Directory/Events.hs` + +**Location:** In `DirectoryCmd` GADT (after line 160, with other user commands) + +**Add:** +```haskell + DCCaptchaMode :: CaptchaMode -> DirectoryCmd 'DRUser +``` + +--- + +### 3.4 Add "audio" tag parsing in Events.hs + +**File:** `apps/simplex-directory-service/src/Directory/Events.hs` + +**Location:** In `tagP` function (after line 205, in user commands section) + +**Add:** +```haskell + "audio" -> u DCCaptchaMode_ +``` + +--- + +### 3.5 Add `DCCaptchaMode_` case in `cmdP` + +**File:** `apps/simplex-directory-service/src/Directory/Events.hs` + +**Location:** In `cmdP` function (after line 237, with other simple commands) + +**Add:** +```haskell + DCCaptchaMode_ -> pure $ DCCaptchaMode CMAudio +``` + +--- + +### 3.6 Add `DCCaptchaMode` case in `directoryCmdTag` + +**File:** `apps/simplex-directory-service/src/Directory/Events.hs` + +**Location:** In `directoryCmdTag` function (after line 316) + +**Add:** +```haskell + DCCaptchaMode _ -> "audio" +``` + +--- + +### 3.7 Update `PendingCaptcha` with `captchaMode` field + +**File:** `apps/simplex-directory-service/src/Directory/Service.hs` + +**Location:** Lines 103-107 + +**Before:** +```haskell +data PendingCaptcha = PendingCaptcha + { captchaText :: Text, + sentAt :: UTCTime, + attempts :: Int + } +``` + +**After:** +```haskell +data PendingCaptcha = PendingCaptcha + { captchaText :: Text, + sentAt :: UTCTime, + attempts :: Int, + captchaMode :: CaptchaMode + } +``` + +--- + +### 3.8 Update import in Service.hs + +**File:** `apps/simplex-directory-service/src/Directory/Service.hs` + +**Location:** Line 41 + +**Before:** +```haskell +import Directory.Events +``` + +**After (no change needed):** The implicit import already imports all exports including the new `CaptchaMode`. + +--- + +### 3.9 Update `sendMemberCaptcha` signature and implementation + +**File:** `apps/simplex-directory-service/src/Directory/Service.hs` + +**Location:** Function `sendMemberCaptcha` (lines 569-589) + +**Before:** +```haskell + sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> IO () + sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts = do + s <- getCaptchaStr captchaLength "" + mc <- getCaptcha s + sentAt <- getCurrentTime + let captcha = PendingCaptcha {captchaText = T.pack s, sentAt, attempts = prevAttempts + 1} + atomically $ TM.insert gmId captcha $ pendingCaptchas env + sendCaptcha mc + where + getCaptcha s = case captchaGenerator opts of + Nothing -> pure textMsg + Just script -> content <$> readProcess script [s] "" + where + textMsg = MCText $ T.pack s + content r = case T.lines $ T.pack r of + [] -> textMsg + "" : _ -> textMsg + img : _ -> MCImage "" $ ImageData img + sendRef = SRGroup groupId $ Just $ GCSMemberSupport (Just gmId) + sendCaptcha mc = sendComposedMessages_ cc sendRef [(quotedId, MCText noticeText), (Nothing, mc)] + gmId = groupMemberId' m +``` + +**After:** +```haskell + sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> CaptchaMode -> IO () + sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts mode = do + s <- getCaptchaStr captchaLength "" + sentAt <- getCurrentTime + let captcha = PendingCaptcha {captchaText = T.pack s, sentAt, attempts = prevAttempts + 1, captchaMode = mode} + atomically $ TM.insert gmId captcha $ pendingCaptchas env + case mode of + CMAudio -> do + sendComposedMessages_ cc sendRef [(quotedId, MCText noticeText)] + sendVoiceCaptcha sendRef s + CMText -> do + mc <- getCaptcha s + sendCaptcha mc + where + getCaptcha s = case captchaGenerator opts of + Nothing -> pure textMsg + Just script -> content <$> readProcess script [s] "" + where + textMsg = MCText $ T.pack s + content r = case T.lines $ T.pack r of + [] -> textMsg + "" : _ -> textMsg + img : _ -> MCImage "" $ ImageData img + sendRef = SRGroup groupId $ Just $ GCSMemberSupport (Just gmId) + sendCaptcha mc = sendComposedMessages_ cc sendRef [(quotedId, MCText noticeText), (Nothing, mc)] + gmId = groupMemberId' m +``` + +--- + +### 3.10 Update `dePendingMember` call site + +**File:** `apps/simplex-directory-service/src/Directory/Service.hs` + +**Location:** Line 561 + +**Before:** +```haskell + | memberRequiresCaptcha a m = sendMemberCaptcha g m Nothing captchaNotice 0 +``` + +**After:** +```haskell + | memberRequiresCaptcha a m = sendMemberCaptcha g m Nothing captchaNotice 0 CMText +``` + +--- + +### 3.11 Make `/audio` clickable in `captchaNotice` + +**File:** `apps/simplex-directory-service/src/Directory/Service.hs` + +**Location:** `dePendingMember` function, `captchaNotice` definition (lines 565-567) + +**Before:** +```haskell + captchaNotice = + "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." + <> if isJust (voiceCaptchaGenerator opts) then "\nSend /audio to receive a voice captcha." else "" +``` + +**After:** +```haskell + captchaNotice = + "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." + <> if isJust (voiceCaptchaGenerator opts) then "\nSend /'audio' to receive a voice captcha." else "" +``` + +--- + +### 3.12 Refactor `dePendingMemberMsg` with inverted structure + +**File:** `apps/simplex-directory-service/src/Directory/Service.hs` + +**Location:** `dePendingMemberMsg` function (lines 618-656) + +**Key changes:** +1. Parse command FIRST using existing `directoryCmdP` +2. Do TM.lookup ONCE (not per-branch) +3. Case on lookup result, then on command inside + +**Before:** +```haskell + dePendingMemberMsg :: GroupInfo -> GroupMember -> ChatItemId -> Text -> IO () + dePendingMemberMsg g@GroupInfo {groupId, groupProfile = GroupProfile {displayName = n}} m@GroupMember {memberProfile = LocalProfile {displayName}} ciId msgText + | memberRequiresCaptcha a m = do + let gmId = groupMemberId' m + sendRef = SRGroup groupId $ Just $ GCSMemberSupport (Just gmId) + if T.toLower (T.strip msgText) == "/audio" + then + atomically (TM.lookup gmId $ pendingCaptchas env) >>= \case + Just PendingCaptcha {captchaText} -> + sendVoiceCaptcha sendRef (T.unpack captchaText) + Nothing -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 + else do + ts <- getCurrentTime + atomically (TM.lookup gmId $ pendingCaptchas env) >>= \case + Just PendingCaptcha {captchaText, sentAt, attempts} + | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired $ attempts - 1 + | matchCaptchaStr captchaText msgText -> do + sendComposedMessages_ cc sendRef [(Just ciId, MCText $ "Correct, you joined the group " <> n)] + approvePendingMember a g m + | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts + | otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts + Nothing -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 + | otherwise = approvePendingMember a g m + where + a = groupMemberAcceptance g + rejectPendingMember rjctNotice = do + let gmId = groupMemberId' m + sendComposedMessages cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [MCText rjctNotice] + sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case + Right (CRUserDeletedMembers _ _ (_ : _) _) -> do + atomically $ TM.delete gmId $ pendingCaptchas env + logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g + r -> logError $ "unexpected remove member response: " <> tshow r + captchaExpired = "Captcha expired, please try again." + wrongCaptcha attempts + | attempts == maxCaptchaAttempts - 1 = "Incorrect text, please try again - this is your last attempt." + | otherwise = "Incorrect text, please try again." + noCaptcha = "Unexpected message, please try again." + tooManyAttempts = "Too many failed attempts, you can't join group." +``` + +**After:** +```haskell + dePendingMemberMsg :: GroupInfo -> GroupMember -> ChatItemId -> Text -> IO () + dePendingMemberMsg g@GroupInfo {groupId, groupProfile = GroupProfile {displayName = n}} m@GroupMember {memberProfile = LocalProfile {displayName}} ciId msgText + | memberRequiresCaptcha a m = do + let gmId = groupMemberId' m + sendRef = SRGroup groupId $ Just $ GCSMemberSupport (Just gmId) + cmd = fromRight (ADC SDRUser DCUnknownCommand) $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.strip msgText + atomically (TM.lookup gmId $ pendingCaptchas env) >>= \case + Nothing -> + let mode = case cmd of ADC SDRUser (DCCaptchaMode CMAudio) -> CMAudio; _ -> CMText + in sendMemberCaptcha g m (Just ciId) noCaptcha 0 mode + Just pc@PendingCaptcha {captchaText, sentAt, attempts, captchaMode} -> case cmd of + ADC SDRUser (DCCaptchaMode CMAudio) -> do + atomically $ TM.insert gmId pc {captchaMode = CMAudio} $ pendingCaptchas env + sendVoiceCaptcha sendRef (T.unpack captchaText) + ADC SDRUser (DCSearchGroup _) -> do + ts <- getCurrentTime + if + | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired (attempts - 1) captchaMode + | matchCaptchaStr captchaText msgText -> do + sendComposedMessages_ cc sendRef [(Just ciId, MCText $ "Correct, you joined the group " <> n)] + approvePendingMember a g m + | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts + | otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts captchaMode + _ -> sendComposedMessages_ cc sendRef [(Just ciId, MCText unknownCommand)] + | otherwise = approvePendingMember a g m + where + a = groupMemberAcceptance g + rejectPendingMember rjctNotice = do + let gmId = groupMemberId' m + sendComposedMessages cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [MCText rjctNotice] + sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case + Right (CRUserDeletedMembers _ _ (_ : _) _) -> do + atomically $ TM.delete gmId $ pendingCaptchas env + logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g + r -> logError $ "unexpected remove member response: " <> tshow r + captchaExpired = "Captcha expired, please try again." + wrongCaptcha attempts + | attempts == maxCaptchaAttempts - 1 = "Incorrect text, please try again - this is your last attempt." + | otherwise = "Incorrect text, please try again." + noCaptcha = "Unexpected message, please try again." + unknownCommand = "Unknown command, please enter captcha text." + tooManyAttempts = "Too many failed attempts, you can't join group." +``` + +--- + +### 3.13 Add imports in Service.hs + +**File:** `apps/simplex-directory-service/src/Directory/Service.hs` + +**Location:** After existing imports (around line 28) + +**Add:** +```haskell +import qualified Data.Attoparsec.Text as A +import Data.Either (fromRight) +``` + +**Note:** `T.strip` is already available via the existing `import qualified Data.Text as T`. + +--- + +## Test Updates + +**File:** `tests/Bots/DirectoryTests.hs` + +### 4.1 Update expected output for clickable command + +**Location:** Line 1278 (or wherever `"Send /audio"` appears) + +**Before:** +```haskell +cath <## "Send /audio to receive a voice captcha." +``` + +**After:** +```haskell +cath <## "Send /'audio' to receive a voice captcha." +``` + +### 4.2 Add test for audio captcha retry behavior + +**Location:** New test function `testVoiceCaptchaRetry` after `testVoiceCaptchaScreening` + +**Strategy:** Add test that verifies wrong answer after `/audio` sends voice retry (not image). + +```haskell +testVoiceCaptchaRetry :: HasCallStack => TestParams -> IO () +testVoiceCaptchaRetry ps = do + -- Setup similar to testVoiceCaptchaScreening... + -- After receiving initial image captcha and switching to audio: + -- cath requests audio captcha + cath #> "#privacy (support) /audio" + cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)" + cath <#. "#privacy (support) 'SimpleX Directory'> sends file " + cath <##. "use /fr 1" + -- cath sends WRONG answer after switching to audio mode + cath #> "#privacy (support) wrong_answer" + cath <# "#privacy (support) 'SimpleX Directory'!> > cath wrong_answer" + cath <## " Incorrect text, please try again." + -- KEY ASSERTION: retry sends VOICE captcha (not image) because captchaMode=CMAudio + cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)" + cath <#. "#privacy (support) 'SimpleX Directory'> sends file " + cath <##. "use /fr 2" +``` + +--- + +## Files Changed + +| File | Changes | +|------|---------| +| `apps/simplex-directory-service/src/Directory/Events.hs` | Add `CaptchaMode` type; add `DCCaptchaMode_` tag; add `DCCaptchaMode` constructor; add "audio" tag parsing; add `cmdP` case; add `directoryCmdTag` case; export `directoryCmdP`; update exports | +| `apps/simplex-directory-service/src/Directory/Service.hs` | Add imports (`Data.Attoparsec.Text`, `Data.Either.fromRight`); update `PendingCaptcha` with `captchaMode :: CaptchaMode`; update `sendMemberCaptcha` signature; refactor `dePendingMemberMsg` with inverted structure; make `/audio` clickable | +| `tests/Bots/DirectoryTests.hs` | Update expected output (`/'audio'`); add `testVoiceCaptchaRetry` | + +--- + +## Summary of Changes + +1. **New type in Events.hs:** + - `data CaptchaMode = CMText | CMAudio` + +2. **New constructor in DirectoryCmd GADT:** + - `DCCaptchaMode :: CaptchaMode -> DirectoryCmd 'DRUser` + - Uses existing Attoparsec parsing infrastructure via `directoryCmdP` + +3. **State tracking (Service.hs):** + - `PendingCaptcha { ..., captchaMode :: CaptchaMode }` + +4. **Refactored `dePendingMemberMsg` (Service.hs):** + - Parses command FIRST using `directoryCmdP` + - Does `TM.lookup` ONCE (inverted structure, no duplication) + - `Nothing` case: send new captcha in mode derived from command + - `Just pc` case: switch on command type + - `DCCaptchaMode CMAudio` → set mode, send voice captcha + - `DCSearchGroup _` → captcha answer (verify/retry) + - `_` → unknown command (error message) + +5. **Updated `sendMemberCaptcha` (Service.hs):** + - Takes `CaptchaMode` parameter instead of `Bool` + - Sends voice or image captcha based on mode + +6. **Clickable command:** + - `"Send /'audio'"` instead of `"Send /audio"` + +7. **Test coverage:** + - `testVoiceCaptchaScreening` (updated): verify clickable command format + - `testVoiceCaptchaRetry` (new): verify retry behavior with `captchaMode` persistence diff --git a/plans/directory-tests-coverage.md b/plans/directory-tests-coverage.md new file mode 100644 index 0000000000..a17d8b379a --- /dev/null +++ b/plans/directory-tests-coverage.md @@ -0,0 +1,79 @@ +# Directory Modules: Test Coverage Report + +## Final Coverage + +| Module | Expressions | Coverage | Gap | +|---|---|---|---| +| **Captcha** | 84/84 | **100%** | -- | +| **Search** | 3/3 | **100%** | -- | +| **BlockedWords** | 158/158 | **100%** | -- | +| **Events** | 527/559 | **94%** | 32 expr | +| **Options** | 223/291 | **76%** | 68 expr | +| **Store** | 1137/1306 | **87%** | 169 expr | +| **Listing** | 379/650 | **58%** | 271 expr | + +84 tests, 0 failures. + +## What was covered + +Tests added to `tests/Bots/DirectoryTests.hs`: + +- **Search**: `SearchRequest` field selectors (`searchType`, `searchTime`, `lastGroup`) +- **BlockedWords**: `BlockedWordsConfig` field selectors, `removeTriples` with `'\0'` input to force initial `False` argument +- **Options**: `directoryOpts` parser via `execParserPure` (minimal args, non-default args, all `MigrateLog` variants), `mkChatOpts` remaining fields +- **Events**: command parser edge cases (`/`, `/filter 1 name=all`, `/submit`, moderate/strong presets), `Show` instances for `DirectoryCmdTag`, `DirectoryCmd`, `SDirectoryRole`, `DirectoryHelpSection`, `DirectoryEvent`, `ADirectoryCmd` (including `showList`), `DCApproveGroup` field selectors via `OverloadedRecordDot`, `CEvtChatErrors` path +- **Store**: `Show` instances for `GroupRegStatus` constructors, `ProfileCondition`, `noJoinFilter`, `GroupReg.createdAt` field +- **Listing**: `DirectoryEntryType` JSON round-trip with field selectors + +Source changes: + +- `Directory/Options.hs`: exported `directoryOpts` +- `Directory/Events.hs`: exported `DirectoryCmdTag (..)` + +## Why not 100% + +### Events (32 expr remaining) + +**Field selectors (9 expr)** on `DEGroupInvitation`, `DEServiceJoinedGroup`, `DEGroupUpdated` -- need `Contact`, `GroupInfo`, `GroupMember` types which have 20+ nested required fields each with no test constructors available. + +**`crDirectoryEvent_` branches (3 expr)**: `DEItemDeleteIgnored`, `DEUnsupportedMessage`, `CEvtMessageError` -- need `AChatItem` or `User`, both strict-data types with deep dependency chains impossible to construct in unit tests. + +**`DCSubmitGroup` paths (2 expr)**: constructor and `directoryCmdTag` case -- need a valid `ConnReqContact` (SMP queue URI with cryptographic keys). + +**Lazy `fail` strings (2 expr)**: `"bad command tag"` and `"bad help section"` -- Attoparsec discards the string argument to `fail` without evaluating it. Inherently uncoverable by HPC. + +### Options (68 expr remaining) + +**Parser metadata strings (~50 expr)**: `metavar` and `help` string literals in `optparse-applicative` option declarations are evaluated lazily by the library. `execParserPure` constructs the parser but doesn't force help strings unless `--help` is invoked. + +**`getDirectoryOpts` (~10 expr)**: wraps `execParser` which reads process `argv` -- can't unit-test without spawning a process. + +**`parseKnownGroup` internals (~8 expr)**: the `--owners-group` arg is parsed but the `KnownContacts` parser internals are instrumented separately. + +### Store (169 expr remaining) + +**DB operations (~150 expr)**: `withDB'` wrappers, SQL query strings, error message literals inside database functions (`setGroupStatusStore`, `setGroupRegOwnerStore`, `searchListedGroups`, `getAllGroupRegs_`, etc.) -- all require a running SQLite database with realistic data. + +**Pagination branches (~15 expr)**: `searchListedGroups` and `getAllGroupRegs_` cursor pagination -- need multi-page result sets. + +**Parser failure (~4 expr)**: `GroupRegStatus` `strDecode` failure path -- needs malformed stored data. + +### Listing (271 expr remaining) + +**Image processing (~80 expr)**: `imgFileData`, image file Base64 encoding paths -- require groups with profile images. + +**Listing generation (~120 expr)**: `generateListing`, `groupDirectoryEntry` -- require `GroupInfo` (21+ fields), `GroupLink`, `CreatedLinkContact` types with deep nesting into chat protocol internals. + +**Field selectors (~40 expr)**: `DirectoryEntry` fields (`displayName`, `fullName`, `image`, `memberCount`, etc.) -- need full `DirectoryEntry` construction which requires `CreatedLinkContact`. + +**TH-generated JSON (~30 expr)**: Template Haskell `deriveJSON` expressions are marked as runtime-uncovered by HPC despite executing at compile time. + +## Summary + +All remaining gaps fall into three categories: + +1. **DB integration paths** -- require a running database (Store) +2. **Complex chat protocol types** -- types with 20+ required nested fields (Events, Listing) +3. **Lazy evaluation artifacts** -- HPC can't observe values that are never forced at runtime (Options `help` strings, Attoparsec `fail` strings, TH-generated code) + +None are testable with pure unit tests without either standing up a database or constructing massive type hierarchies. diff --git a/plans/website-file-page-implementation.md b/plans/website-file-page-implementation.md new file mode 100644 index 0000000000..bca77e77a9 --- /dev/null +++ b/plans/website-file-page-implementation.md @@ -0,0 +1,472 @@ +# File Transfer Page — Implementation Plan + +## Table of Contents +1. [Context](#1-context) +2. [Executive Summary](#2-executive-summary) +3. [High-Level Design](#3-high-level-design) +4. [Detailed Implementation Plan](#4-detailed-implementation-plan) +5. [Known Divergences from Product Plan](#5-known-divergences-from-product-plan) +6. [Verification](#6-verification) + +--- + +## 1. Context + +**Problem**: The website needs a `/file` page that lets users upload/download files via XFTP servers directly in the browser — a live demo that funnels users toward downloading the SimpleX app. + +**Product plan**: `plans/website-file-page-product.md` + +**Approach**: Use the pre-built `dist-web/` bundle from `@shhhum/xftp-web@0.8.0`. Copy three files (`index.js` + `index.css` + `crypto.worker.js`) to website static assets. Wrap with an 11ty page providing the protocol overlay, app download CTA, and i18n bridge. **No Vite/TS build step.** The bundle handles all XFTP protocol, crypto, Web Worker, upload/download UI. + +**Library features used** (v0.8.0): +- `data-xftp-app` — configurable target element +- `data-no-hashchange` — prevents conflict with overlay system +- `window.__XFTP_I18N__` — string externalization for i18n +- `xftp:upload-complete` / `xftp:download-complete` — CustomEvents for CTA injection +- Scoped CSS (`#app` / `.dark #app`) — no global resets +- Relative worker URL — both files co-located in same directory + +**Routing**: `/file/` (no hash) = upload mode; `/file/#` = download mode. + +--- + +## 2. Executive Summary + +| Action | Files | +|--------|-------| +| **Create** | `website/src/file.html`, `website/src/_data/file_overlays.json`, `website/src/_includes/overlay_content/file/protocol.html` | +| **Copy from npm** | `dist-web/assets/index.js` + `dist-web/assets/index.css` + `dist-web/assets/crypto.worker.js` → `src/file-assets/` | +| **Modify** | `website/package.json`, `website/.eleventy.js`, `website/src/_includes/navbar.html`, `website/langs/en.json` (~30 keys), `website/web.sh`, `website/src/js/script.js`, `.gitignore` | + +--- + +## 3. High-Level Design + +### Architecture + +``` +website/src/ +├── file.html # 11ty page +├── _data/file_overlays.json # overlay config (showImage: false for v1) +├── _includes/overlay_content/file/ +│ └── protocol.html # protocol popup content +└── file-assets/ # COPIED from npm dist-web/assets/ (gitignored) + ├── index.js # main bundle (~1.1 MB) + ├── index.css # scoped CSS (~2.3 KB) + └── crypto.worker.js # worker (~1.0 MB) +``` + +### Data flow + +**Upload**: `#app` div → bundle renders drop zone → file input → Worker encrypts (OPFS) → `uploadFile()` → share link → `xftp:upload-complete` event → website shows inline CTA + +**Download**: hash parsed by bundle on init → `decodeDescriptionURI()` → download button → Worker decrypts → browser save → `xftp:download-complete` event → website shows inline CTA + +### Overlay conflict resolution + +Bundle's `hashchange` listener is disabled via `data-no-hashchange` attribute. Protocol overlay opens via **direct DOM manipulation** (inline JS `classList.remove('hidden')`) — not hash-based. script.js's global `.close-overlay-btn` handler still closes it. No hash events fired when opening. + +Note: `closeOverlay()` in script.js calls `history.replaceState(null, null, ' ')` which clears the URL hash. In download mode (`/file/#simplex:...`), this means the hash disappears from the URL bar after closing the overlay. This is cosmetic only — the bundle parses the hash once on init and doesn't re-read it. Download continues unaffected. + +A null guard is added to `openOverlay()` in script.js (Step 9) to prevent crashes when the hash is an XFTP URI fragment rather than a DOM element ID. + +### i18n bridge + +The 11ty template renders `window.__XFTP_I18N__` from en.json keys. The bundle reads via `t(key, fallback)`. All JS-rendered strings are overridable. The bundle renders strings via template literals into innerHTML, so HTML in i18n values (e.g. links in `maxSizeHint`) is rendered correctly. + +--- + +## 4. Detailed Implementation Plan + +### Step 1: Add npm dependency + +**Modify**: `website/package.json` + +```diff + "dependencies": { ++ "@shhhum/xftp-web": "^0.8.0", + } +``` + +### Step 2: Copy dist-web files in web.sh + +**Modify**: `website/web.sh` + +After the existing `cp node_modules/...` lines (after line 30): + +```bash +mkdir -p src/file-assets +cp node_modules/@shhhum/xftp-web/dist-web/assets/index.js src/file-assets/ +cp node_modules/@shhhum/xftp-web/dist-web/assets/index.css src/file-assets/ +cp node_modules/@shhhum/xftp-web/dist-web/assets/crypto.worker.js src/file-assets/ +``` + +Add `file.html` to language copy loop (after line 42, `cp src/fdroid.html src/$lang`): +```bash + cp src/file.html src/$lang +``` + +### Step 3: Create 11ty page — `website/src/file.html` + +``` +--- +layout: layouts/main.html +title: "SimpleX File Transfer" +description: "Send files securely with end-to-end encryption" +templateEngineOverride: njk +active_file: true +--- +{% set lang = page.url | getlang %} +{% from "components/macro.njk" import overlay %} +``` + +**Structure** (top to bottom): + +1. **Noscript fallback**: + ```html + + ``` + +2. **Page section** with centered container: + - `

` with i18n title + - `
` — bundle renders here, hashchange disabled + - Static "E2E encrypted" note below `#app`: + ```html +

+ {{ "file-e2e-note" | i18n({}, lang) | safe }} +

+ ``` + - "Learn more" link (opens overlay via inline JS, not hash): + ```html +

+ + {{ "file-learn-more" | i18n({}, lang) | safe }} + +

+ ``` + +3. **Inline CTA container** (hidden, shown by JS after upload/download): + ```html + + ``` + +4. **Protocol overlay** via existing macro: + ```html + {% for section in file_overlays.sections %} + {{ overlay(section, lang) }} + {% endfor %} + ``` + +5. **Bottom CTA section** (same pattern as `join_simplex.html`): + - Heading: "Get SimpleX — the most private messenger" + - Subheading about the app using the same protocol + - 5 buttons: Apple Store, Google Play, F-Droid, TestFlight, APK (same markup as inline CTA) + +6. **i18n bridge script** (BEFORE bundle load, so `window.__XFTP_I18N__` is set when bundle initializes): + ```html + + ``` + +7. **Overlay open + CTA injection script**: + ```html + + ``` + +8. **Bundle + CSS** (bundle AFTER i18n bridge): + ```html + + + ``` + +### Step 4: Create protocol overlay data + content + +**New file**: `website/src/_data/file_overlays.json` +```json +{ + "sections": [{ + "id": 1, + "imgLight": "", + "imgDark": "", + "overlayContent": { + "overlayId": "xftp-protocol", + "overlayScrollTo": "", + "title": "file-protocol-title", + "showImage": false, + "contentBody": "overlay_content/file/protocol.html" + } + }] +} +``` + +Note: `showImage: false` — protocol diagram SVGs are deferred to a future iteration. The overlay works without images (same as existing overlays when `showImage` is false — the content section spans full width). + +**New file**: `website/src/_includes/overlay_content/file/protocol.html` + +5 blocks with heading + paragraph structure (existing hero overlay cards use plain `

` tags; this overlay uses `

` + `

` inside `

` wrappers since it has titled sections): + +```html +
+

{{ "file-proto-h-1" | i18n({}, lang) | safe }}

+

{{ "file-proto-p-1" | i18n({}, lang) | safe }}

+
+
+

{{ "file-proto-h-2" | i18n({}, lang) | safe }}

+

{{ "file-proto-p-2" | i18n({}, lang) | safe }}

+
+
+

{{ "file-proto-h-3" | i18n({}, lang) | safe }}

+

{{ "file-proto-p-3" | i18n({}, lang) | safe }}

+
+
+

{{ "file-proto-h-4" | i18n({}, lang) | safe }}

+

{{ "file-proto-p-4" | i18n({}, lang) | safe }}

+
+
+

{{ "file-proto-h-5" | i18n({}, lang) | safe }}

+

{{ "file-proto-p-5" | i18n({}, lang) | safe }}

+
+

+ + {{ "file-proto-spec" | i18n({}, lang) | safe }} + +

+``` + +### Step 5: Add navbar link + +**Modify**: `website/src/_includes/navbar.html` + +After Directory `
  • ` block (after line 27, before the `
    ` at line 29): +```html +
    +
  • +``` + +Add `and ('file' not in page.url)` to language-selector exclusion condition (line 137): +``` +{% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('vouchers' not in page.url) and ('file' not in page.url) %} +``` + +### Step 6: Add translation keys + +**Modify**: `website/langs/en.json` — add these keys: + +``` +Navbar: + "file": "File" + +Noscript + static page content: + "file-noscript": "JavaScript is required for file transfer." + "file-e2e-note": "End-to-end encrypted — the server never sees your file." + "file-learn-more": "Learn more about XFTP protocol" + "file-cta-heading": "Get SimpleX — the most private messenger" + "file-cta-subheading": "The file transfer you just used is built on the same protocol as SimpleX Chat — end-to-end encrypted messaging, voice and video calls, groups, and file sharing. No user IDs. No phone numbers." + +i18n bridge (fed to bundle via window.__XFTP_I18N__): + "file-title": "SimpleX File Transfer" + "file-drop-text": "Drag & drop a file here" + "file-drop-hint": "or" + "file-choose": "Choose file" + "file-max-size": "Max 100 MB — the SimpleX app supports up to 1 GB" + "file-encrypting": "Encrypting\u2026" + "file-uploading": "Uploading\u2026" + "file-cancel": "Cancel" + "file-uploaded": "File uploaded" + "file-copy": "Copy" + "file-copied": "Copied!" + "file-share": "Share" + "file-expiry": "Files are typically available for 48 hours." + "file-sec-1": "Your file was encrypted in the browser before upload — the server never sees file contents." + "file-sec-2": "The link contains the decryption key in the hash fragment, which the browser never sends to any server." + "file-sec-3": "For maximum security, use the SimpleX app." + "file-retry": "Retry" + "file-downloading": "Downloading\u2026" + "file-decrypting": "Decrypting\u2026" + "file-download-complete": "Download complete" + "file-download-btn": "Download" + "file-too-large": "File too large (%size%). Maximum is 100 MB. The SimpleX app supports files up to 1 GB." + "file-empty": "File is empty." + "file-invalid-link": "Invalid or corrupted link." + "file-init-error": "Failed to initialize: %error%" + "file-available": "File available (~%size%)" + "file-dl-sec-1": "This file is encrypted \u2014 the server never sees file contents." + "file-dl-sec-2": "The decryption key is in the link\u2019s hash fragment, which your browser never sends to any server." + "file-dl-sec-3": "For maximum security, use the SimpleX app." + "file-workers-required": "Web Workers required \u2014 update your browser" + +Protocol overlay content: + "file-protocol-title": "Why XFTP is the most private file transfer" + "file-proto-h-1": "No accounts, no identifiers" + "file-proto-p-1": "Each file chunk uses a fresh, random credential that is used once and discarded. The server has no concept of \"users\" — it only sees isolated, anonymous chunk operations." + "file-proto-h-2": "Encrypted in your browser" + "file-proto-p-2": "The entire file is encrypted with a random key before upload. The server stores ciphertext it cannot decrypt. The key travels only in the URL fragment, which browsers never send to any server." + "file-proto-h-3": "Triple encryption" + "file-proto-p-3": "Every transfer has three layers: TLS transport encryption, per-recipient transit encryption (unique ephemeral key exchange per download), and file-level end-to-end encryption." + "file-proto-h-4": "Distributed across independent servers" + "file-proto-p-4": "File chunks are split across servers operated by independent parties. No single operator sees all chunks. Even if one operator is compromised, they only see encrypted fragments." + "file-proto-h-5": "Files expire automatically" + "file-proto-p-5": "Files are deleted after approximately 48 hours. There is no persistent storage, no file management, no way to extend expiration. Ephemeral by design." + "file-proto-spec": "Read the XFTP protocol specification →" +``` + +### Step 7: Update .eleventy.js + +**Modify**: `website/.eleventy.js` + +1. Add `"file"` to `supportedRoutes` array (line 56): +```js +const supportedRoutes = ["blog", "contact", "invitation", "messaging", "docs", "fdroid", "file", ""] +``` + +2. Add passthrough copy (after line 306, with the other `addPassthroughCopy` calls): +```js +ty.addPassthroughCopy("src/file-assets") +``` + +### Step 8: Gitignore + +**Modify**: `.gitignore` (project root) — add: +``` +website/src/file-assets/ +``` + +### Step 9: Fix script.js null guard + +**Modify**: `website/src/js/script.js` + +The `openOverlay()` function (line 180) crashes when the URL hash is an XFTP URI fragment (e.g. `#simplex:...`) because `document.getElementById('simplex:...')` returns null, and `el.classList.contains('overlay')` throws a TypeError on null. + +**Change** (line 184-185): +```js +// Before: +const el = document.getElementById(id) +if (el.classList.contains('overlay')) { + +// After: +const el = document.getElementById(id) +if (el && el.classList.contains('overlay')) { +``` + +This is a one-character change (`if (el.classList` → `if (el && el.classList`). It makes `openOverlay()` safely ignore hash fragments that don't correspond to overlay elements — which is correct behavior regardless of the file page (any non-overlay hash should be silently ignored). + +--- + +## 5. Known Divergences from Product Plan + +These are intentional deviations from the product plan, caused by browser constraints or library limitations: + +1. **Download requires a click**: Product plan says "No intermediate 'click to download' step." The bundle shows a "Download" button instead of auto-starting. This is a browser security constraint — triggering a file download requires a user gesture. The button also lets the user see file metadata before downloading. + +2. **No cancel during download**: Product plan specifies a Cancel button during download. The bundle does not implement this. The download is relatively fast (direct HTTPS) and cancellation can be done by closing the tab. + +3. **Protocol diagram deferred**: Product plan describes a protocol flow diagram in the overlay. SVG diagrams are deferred to a future iteration. The overlay ships with text-only content (`showImage: false`). + +4. **Overlay close clears download hash**: When the protocol overlay is opened and closed during download mode, `closeOverlay()` clears the URL hash. This is cosmetic — the bundle already parsed the hash on init and the download is unaffected. The URL bar loses the fragment, but the user received the link from elsewhere and doesn't need to re-copy it. + +--- + +## 6. Verification + +### Build +```bash +cd website +npm install --ignore-scripts +mkdir -p src/file-assets +cp node_modules/@shhhum/xftp-web/dist-web/assets/{index.js,index.css,crypto.worker.js} src/file-assets/ +npm run build +ls _site/file/index.html _site/file-assets/index.js _site/file-assets/index.css _site/file-assets/crypto.worker.js +``` + +### Manual test checklist +``` +Visit /file/ + 1. Navbar "File" link is active + 2.