mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 923e2a43a4 | |||
| f29f94f138 | |||
| 3a35374a03 | |||
| fe0793481c | |||
| 3c2a672799 | |||
| 2476d17394 | |||
| 5ae021bde3 | |||
| 9f46c242fc | |||
| cf36254c1a | |||
| 0d94bf2006 | |||
| 4757150aae | |||
| 0d76ab930f | |||
| 5515e2fe3c | |||
| 508f320b4c | |||
| 176e6c94b2 | |||
| deb650970c | |||
| 36fc2261f9 | |||
| f11c6baaf6 | |||
| 6add250258 | |||
| d15efbdb44 | |||
| 2422f9110c | |||
| b6162c2044 | |||
| 75245cfa02 | |||
| 0d19f058fe | |||
| 911e695187 | |||
| b8c8969162 | |||
| 0610520064 | |||
| 5ae60af73f | |||
| dd6f1015dc | |||
| 74ea10ebef | |||
| 877f991131 | |||
| e556878b2f | |||
| 99a1fee994 | |||
| dfd323a89f | |||
| 5a086ff022 | |||
| bedaa2c287 | |||
| 50037d5232 | |||
| 6756b04da9 | |||
| b12ecf66c5 | |||
| d05403db02 | |||
| 2d8344a61f | |||
| 44afda55e5 | |||
| bfa6eef604 | |||
| 6327b30350 | |||
| f92cac4d55 | |||
| 0204fca27d | |||
| eed844469b | |||
| 59ceae46a0 | |||
| 693188dcff | |||
| 025ff086c8 | |||
| a802bfcb61 | |||
| 1f18bfe2dd | |||
| dc1bc77925 | |||
| ac6383d79a | |||
| 59fec8462c | |||
| d64baa294f | |||
| e7e14af158 | |||
| e094e96669 | |||
| fa56e8ca0d | |||
| 3d5abdcd79 | |||
| 92c4e41b59 | |||
| bbfd0c31da | |||
| d5f627be50 | |||
| f0ca58d53f | |||
| 56f5655693 | |||
| 310a7f99c8 | |||
| d0f7fca8c9 | |||
| 80de08f980 | |||
| 80a0ff0098 | |||
| 9c55de17a3 | |||
| 8c1bc8e70e | |||
| 4659bbbe51 | |||
| f0760d6695 | |||
| 68ffedc1d1 | |||
| 4bdfe8e390 | |||
| f3a396daae | |||
| 6d038e9d7f | |||
| 5d62a439d2 | |||
| 46be278bf5 | |||
| ef6f8958ea | |||
| 78c73b3f8c | |||
| 18dc39d266 | |||
| 41cd10a76f | |||
| 7ccae5d3be | |||
| adbfbb9e77 | |||
| dda7f34599 | |||
| bf5a5dc9e9 | |||
| 84faf4115b | |||
| 1577b90047 | |||
| fc94684d04 | |||
| b92ff80e3a | |||
| 515a84b161 | |||
| d757ef59c0 | |||
| cabcc71545 | |||
| d0a584d15d | |||
| f8ec6d2da2 | |||
| c3b7e06b86 | |||
| d5edcea5d0 | |||
| 8a3d29fca3 | |||
| 6156ee0534 | |||
| 1cad6801b6 | |||
| a73af53c50 | |||
| 8eacd09336 | |||
| 673926b1ee | |||
| 643b7cc381 | |||
| 42db29304a | |||
| 323dbde715 | |||
| b9649a908e | |||
| 2f20f558ad | |||
| 6f868f653a | |||
| 42b529dd4a | |||
| d7eab73687 | |||
| 1deebdf552 | |||
| 20fbb0e1ee | |||
| 6a53e229d1 | |||
| 74ef2d478a | |||
| feeece405d | |||
| 20459a5710 | |||
| a3a8b3581f | |||
| f2571dba91 | |||
| 708ffe617c | |||
| f6ecb94047 | |||
| 2d4b52b037 | |||
| 5257f39aee | |||
| 20f1475856 | |||
| db0295e5a6 | |||
| d019c1bfb5 | |||
| 4542ba1b9a | |||
| 9b982db899 | |||
| a1a72b79b3 | |||
| 0ac9bea63c | |||
| 2bb3ad546e | |||
| 41a6b9a6c9 | |||
| fb7c119afc | |||
| 5910fc245a | |||
| 7d8887eddd | |||
| 37471ae9fa | |||
| 319e2ebe5a | |||
| 49a00f3788 | |||
| 74ee2fb8db | |||
| 29a4a4712c | |||
| 644bd1f594 | |||
| ed6eca8920 | |||
| 47512f786e | |||
| add51186e6 | |||
| 52f63b28ba | |||
| 889c9aab45 | |||
| de5940e709 | |||
| 6e184f735d | |||
| c33ff20d0a | |||
| c67c3c8972 | |||
| f98a5178b3 | |||
| 6dfccdab84 | |||
| 40faa3d43e | |||
| 74f6853815 | |||
| cef6817187 | |||
| 6f547dfb66 | |||
| d6e05f61ac | |||
| 55649b0c4b | |||
| 4d9f8dd244 | |||
| 3880da8a08 | |||
| 5e8666835d | |||
| 2f5619141e | |||
| 4a07d7fd9d | |||
| 1493583db7 | |||
| dc363a0281 | |||
| fcffe3922a | |||
| d546d8041f | |||
| 267caf9b03 | |||
| d55ad1f32a | |||
| be07043b47 | |||
| cc3e6bcd9d | |||
| 43654fdadb | |||
| 774add2380 | |||
| 1133a6e624 | |||
| 0aad1b3d76 | |||
| b58a9d0bab | |||
| 54a74a8586 | |||
| f25474947b | |||
| 9ebd8e4a37 | |||
| 975ad2e149 | |||
| e6350aaec2 | |||
| d373537d6d | |||
| 960581e5f2 | |||
| fccf8f402e | |||
| e0459978f7 | |||
| 72bd7376ca | |||
| 94fbdcde05 | |||
| 89d77e7638 | |||
| d9edee117f | |||
| 6cfdb87924 | |||
| 613940577c | |||
| 43a3e21495 | |||
| b5375b8c0e | |||
| 39aec04fea | |||
| 94e2c8dbed | |||
| da45c7bc1c | |||
| d180704d52 | |||
| 653c8688b7 | |||
| 9a382a4948 | |||
| 74e14bd0ea | |||
| 844231e5e7 | |||
| 0ccf0a9309 | |||
| a57122a902 | |||
| 464cba299a | |||
| 655ed8cad4 | |||
| 1d94fe8bca |
@@ -1,26 +0,0 @@
|
||||
name: add artifact links to pull request
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Upload Preview APK"]
|
||||
types: [completed]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
artifacts-url-comments:
|
||||
name: add artifact links to pull request
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: add artifact links to pull request
|
||||
uses: tonyhallett/artifacts-url-comments@0965ff1a7ae03c5c1644d3c30f956effea4e05ef # v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
prefix: "**To test the changes in this pull request, install this apk:**"
|
||||
format: "[📦 {name}]({url})"
|
||||
addTo: pull
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
ndk-version: "r29"
|
||||
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Cache compiled core
|
||||
id: cache-core
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
jni/arm64-v8a
|
||||
|
||||
@@ -18,14 +18,16 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- uses: actions/cache@v4
|
||||
- name: Restore Gradle cache
|
||||
id: gradle-cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -37,3 +39,11 @@ jobs:
|
||||
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
- name: Check formatting
|
||||
run: ./gradlew spotlessCheck
|
||||
- name: Save Gradle cache
|
||||
if: github.event_name == 'push' && steps.gradle-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
|
||||
@@ -12,21 +12,24 @@ jobs:
|
||||
build:
|
||||
name: Upload Preview APK
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate Fastlane Metadata
|
||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2
|
||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@c8857fdbbd3e00f9a5cbe8604bcecfa95ce8fef8 # v2.1.0
|
||||
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'temurin'
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -42,7 +45,7 @@ jobs:
|
||||
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
ndk-version: "r29"
|
||||
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
@@ -54,7 +57,7 @@ jobs:
|
||||
|
||||
- name: Restore compiled core
|
||||
id: core-cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
jni/arm64-v8a
|
||||
@@ -73,7 +76,24 @@ jobs:
|
||||
run: ./gradlew --no-daemon -PABI_FILTER=arm64-v8a assembleFossDebug
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
id: upload
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: app-preview.apk
|
||||
path: 'build/outputs/apk/foss/debug/*.apk'
|
||||
|
||||
- name: Add artifact links to PR
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
|
||||
with:
|
||||
script: |
|
||||
const url = process.env.ARTIFACT_URL;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: `**To test the changes in this pull request, install this apk:**\n\n[📦 app-preview.apk](${url})`,
|
||||
});
|
||||
|
||||
@@ -23,18 +23,18 @@ jobs:
|
||||
name: Upload Release APK
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
working-directory: jni/deltachat-core-rust
|
||||
- uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'temurin'
|
||||
- uses: android-actions/setup-android@v3
|
||||
- uses: nttld/setup-ndk@v1
|
||||
- uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
||||
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
mv build/outputs/mapping/gplayRelease/mapping.txt build/outputs/mapping/fossRelease/mapping-gplay.txt
|
||||
|
||||
- name: Release on GitHub
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
body: '[<img src="store/get-it-on-gplay.png" alt="Get it on Google Play" height="48">](https://play.google.com/store/apps/details?id=com.github.arcanechat) [<img src="store/get-it-on-fdroid.png" alt="Get it on F-Droid" height="48">](https://f-droid.org/packages/chat.delta.lite) [<img src="store/get-it-on-github.png" alt="Get it on GitHub" height="48">](https://github.com/ArcaneChat/android/releases/latest/download/ArcaneChat-gplay.apk)'
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
+5
-5
@@ -47,7 +47,7 @@ Nix development environment contains Rust with cross-compilation toolchains and
|
||||
|
||||
To [build an APK](https://developer.android.com/studio/build/building-cmdline) run the following 2 steps.
|
||||
Note that the first step may take some time to build for all architectures. You can optionally read
|
||||
[the first comment block in the `ndk-make.sh` script](https://github.com/deltachat/deltachat-android/blob/master/scripts/ndk-make.sh)
|
||||
[the first comment block in the `ndk-make.sh` script](./scripts/ndk-make.sh)
|
||||
for pointers on how to build for a specific architecture.
|
||||
```
|
||||
$ scripts/ndk-make.sh
|
||||
@@ -56,7 +56,7 @@ $ ./gradlew assembleDebug
|
||||
|
||||
Resulting APK files can be found in
|
||||
`build/outputs/apk/gplay/debug/` and
|
||||
`build/outputs/apk/fat/debug/`.
|
||||
`build/outputs/apk/foss/debug/`.
|
||||
|
||||
## Build Using Dockerfile
|
||||
|
||||
@@ -114,7 +114,7 @@ deltachat@6012dcb974fe:/home/app$ ./gradlew assembleDebug
|
||||
|
||||
In /etc/containers/storage.conf, replace the line: `driver = ""` with: `driver = "overlay"`.
|
||||
You can also set the `driver` option to something else, you just need to set it to _something_.
|
||||
[Read about possible options here](https://github.com/containers/storage/blob/master/docs/containers-storage.conf.5.md#storage-table).
|
||||
[Read about possible options here](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md#storage-table).
|
||||
|
||||
## <a name="setup-podman"></a>Setup Podman
|
||||
|
||||
@@ -135,8 +135,8 @@ See https://wiki.archlinux.org/index.php/Podman#Rootless_Podman for more informa
|
||||
|
||||
To setup build environment manually:
|
||||
- _Either_, in Android Studio, go to "Tools / SDK Manager / SDK Tools", enable "Show Package Details",
|
||||
select "CMake" and the desired NDK (install the same NDK version as the [Dockerfile](https://github.com/deltachat/deltachat-android/blob/master/Dockerfile)), hit "Apply".
|
||||
- _Or_ read [Dockerfile](https://github.com/deltachat/deltachat-android/blob/master/Dockerfile) and mimic what it does.
|
||||
select "CMake" and the desired NDK (install the same NDK version as the [Dockerfile](./Dockerfile)), hit "Apply".
|
||||
- _Or_ read [Dockerfile](./Dockerfile) and mimic what it does.
|
||||
|
||||
Then, in both cases, install Rust using [rustup](https://rustup.rs/)
|
||||
and Rust toolchains for cross-compilation by executing `scripts/install-toolchains.sh`.
|
||||
|
||||
@@ -2,20 +2,77 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
* Allow to select multiple files for sending
|
||||
* Add notifications for missed calls
|
||||
* Video call preview now accurately shows what is sent to remote
|
||||
* Fix: properly hide draft attachment during in-chat search
|
||||
* Fix: close mini-apps and chats if they are deleted
|
||||
* Fix: cancel in-chat search when back is pressed, instead of directly returning to chatlist
|
||||
|
||||
## v2.53.0
|
||||
2026-06
|
||||
|
||||
* Use message style notifications for longer message previews
|
||||
* Remove notification after audio playback ends
|
||||
* Fix: do not allow blocked contacts to use our invite links
|
||||
* Fix sending mini-app that was used/prepared before sending
|
||||
* Some more small fixes and updated translations
|
||||
* Update to core 2.53.0
|
||||
|
||||
## v2.52.0
|
||||
2026-06
|
||||
|
||||
* Fix: avoid crashes in Media preview sometimes
|
||||
* Fix: Incorrect total time when attaching audio files as draft
|
||||
* Fix: Audio files in draft showing total time from wrong file
|
||||
* Fix: Update the channel title after joining if the QR code had an outdated title
|
||||
* Voice recording will be automatically saved as draft when interrupted
|
||||
* Update to core 2.52.0
|
||||
|
||||
## v2.51.0
|
||||
2026-06
|
||||
|
||||
* Better incoming call system integration
|
||||
* Calls are not experimental anymore and don't need to be manually enabled
|
||||
* Calls can be answered by tapping messages
|
||||
* Notify the user when they try to make a call while the device is offline
|
||||
* Channels are no longer experimental and are available by default
|
||||
* Display a permanent notification when doing location streaming and get rid of dangerous "Access Location in Background" permission
|
||||
* Autoplay all voice messages in a chat
|
||||
* Allow to share location for 24 hours
|
||||
* Allow mini-apps to play audio without user interaction
|
||||
* Allow to paste and open invitation links from search
|
||||
* Mark chats as unread (long tap a chat and select the corresponding option from the three-dot-menu)
|
||||
* Add "Mark all as read" option to profile menu in the profile switcher
|
||||
* Fix process of upgrading from a very old version of the app
|
||||
* Show more recent added stickers at the top of the sticker picker
|
||||
* Allow to open links in messages via actions in TalkBack menu
|
||||
* Allow to open map if user clicks "Location streaming enabled" system message
|
||||
* Allow to disable incoming calls notifications
|
||||
* Add an option to process unencrypted messages; by default, only encrypted messages can be sent or received
|
||||
* Fix: do not accidentally set draft in chats that don't allow sending messages
|
||||
* Fix swipe navigation between tabs in RTL languages
|
||||
* Remove "Move to DeltaChat folder", in case you are using the option, a device message shows how to proceed
|
||||
* Remove "Only fetch from DeltaChat folder" option, the functionality is preserved for existing profiles
|
||||
* Remove "Delete Messages from Server" option, this is now up to the server:
|
||||
Chatmail handles that automatically, classic email servers used as relay often have lots of storage or options themselves
|
||||
* Remove "Show Email" options, all messages are shown by default, shared usage of email account is not supported
|
||||
* Allow otherwise invalid TLS connections if the key is unchanged
|
||||
* Adapt quota warning to automatic cleanup.
|
||||
* Don't show non-delivery-notfications in broadcast channels
|
||||
* Resend the last 10 messages to new broadcast channel member
|
||||
* Enable PQC (Post-Quantum Cryptography) support. We do not generate PQC keys yet, this step is needed for forward compatibility
|
||||
* Improve avatar quality
|
||||
* Add new webxdc.isAppSender and webxdc.isBroadcast APIs for mini-apps
|
||||
* Fix: avoid invalid empty "~" notifications when some peer is streaming location
|
||||
* Fix: Improve detection of stickers
|
||||
* Fix text direction issues for RTL languages in "Show full message" view
|
||||
* Fix: Reconnect when removing a relay
|
||||
* Fix: Location streaming now works correctly with multiple accounts
|
||||
* Fix debouncing in chatlist and search
|
||||
* Fix sharing contact across profiles
|
||||
* Fix: Reset scroll location after switching account
|
||||
* Update to core 2.51.0
|
||||
|
||||
## v2.49.0
|
||||
2026-04
|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
defaultConfig {
|
||||
versionCode 30000742
|
||||
versionName "2.49.0"
|
||||
versionCode 30000746
|
||||
versionName "2.53.0"
|
||||
|
||||
applicationId "chat.delta.lite"
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
buildInputs = [
|
||||
android-sdk
|
||||
pkgs.openjdk17
|
||||
pkgs.perl
|
||||
(pkgs.buildPackages.rust-bin.stable."${rust-version}".minimal.override {
|
||||
targets = [
|
||||
"armv7-linux-androideabi"
|
||||
|
||||
@@ -1643,12 +1643,6 @@ JNIEXPORT void Java_com_b44t_messenger_DcMsg_setHtml(JNIEnv *env, jobject obj, j
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT void Java_com_b44t_messenger_DcMsg_forceSticker(JNIEnv *env, jobject obj)
|
||||
{
|
||||
dc_msg_force_sticker(get_dc_msg(env, obj));
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getPOILocation(JNIEnv *env, jobject obj)
|
||||
{
|
||||
char* temp = dc_msg_get_poi_location(get_dc_msg(env, obj));
|
||||
|
||||
+1
-1
Submodule jni/deltachat-core-rust updated: 570119830f...9435042594
@@ -493,6 +493,11 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="camera|microphone|phoneCall" />
|
||||
|
||||
<receiver
|
||||
android:name=".calls.CallActionReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".notifications.MarkReadReceiver"
|
||||
android:enabled="true"
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
<ul>
|
||||
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
|
||||
@@ -628,6 +643,197 @@ but more than 150 is not recommended.</p>
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1425,23 +1631,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#wie-viele-mitglieder-können-in-einer-einzelnen-gruppe-sein">Wie viele Mitglieder können in einer einzelnen Gruppe sein?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Kanäle</a>
|
||||
<ul>
|
||||
<li><a href="#einem-kanal-beitreten">Einem Kanal beitreten</a></li>
|
||||
<li><a href="#einen-kanal-erstellen">Einen Kanal erstellen</a></li>
|
||||
<li><a href="#wie-viele-empfänger-kann-ein-kanal-haben">Wie viele Empfänger kann ein Kanal haben?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Anrufe</a>
|
||||
<ul>
|
||||
<li><a href="#jemanden-anrufen">Jemanden anrufen</a></li>
|
||||
<li><a href="#einen-anruf-annehmen-oder-ablehnen">Einen Anruf annehmen oder ablehnen</a></li>
|
||||
<li><a href="#während-des-anrufs">Während des Anrufs</a></li>
|
||||
<li><a href="#verpasste-anrufe-und-benachrichtigungen">Verpasste Anrufe und Benachrichtigungen</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-Chat-Apps</a>
|
||||
<ul>
|
||||
<li><a href="#wo-bekomme-ich-in-chat-apps">Wo bekomme ich In-Chat-Apps?</a></li>
|
||||
@@ -584,6 +599,197 @@ aber mehr als 150 sind nicht empfohlen.</p>
|
||||
|
||||
<p>Wenn Gruppen größer werden, können sie sozial instabil werden und benötigen möglicherweise eine Hierarchie - und Delta Chat ist ein privater Messenger für Chats mit <a href="#groups">gleichen Rechten</a>. Vgl. <a href="https://de.wikipedia.org/wiki/Dunbar-Zahl">Dunbar-Zahl</a>.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Kanäle <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Kanäle dienen der Verbreitung von Nachrichten an viele Empfänger.</p>
|
||||
|
||||
<h3 id="einem-kanal-beitreten">
|
||||
|
||||
|
||||
Einem Kanal beitreten <a href="#einem-kanal-beitreten" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="einen-kanal-erstellen">
|
||||
|
||||
|
||||
Einen Kanal erstellen <a href="#einen-kanal-erstellen" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="wie-viele-empfänger-kann-ein-kanal-haben">
|
||||
|
||||
|
||||
Wie viele Empfänger kann ein Kanal haben? <a href="#wie-viele-empfänger-kann-ein-kanal-haben" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Anrufe <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="jemanden-anrufen">
|
||||
|
||||
|
||||
Jemanden anrufen <a href="#jemanden-anrufen" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="einen-anruf-annehmen-oder-ablehnen">
|
||||
|
||||
|
||||
Einen Anruf annehmen oder ablehnen <a href="#einen-anruf-annehmen-oder-ablehnen" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="während-des-anrufs">
|
||||
|
||||
|
||||
Während des Anrufs <a href="#während-des-anrufs" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="verpasste-anrufe-und-benachrichtigungen">
|
||||
|
||||
|
||||
Verpasste Anrufe und Benachrichtigungen <a href="#verpasste-anrufe-und-benachrichtigungen" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1329,23 +1535,16 @@ im Falle einer Beschlagnahmung des Geräts nicht ohne Weiteres identifiziert wer
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Das verwendete <a href="#relays">Relay</a> muss Ihre IP-Adresse kennen,
|
||||
sowie manchmal auch die Geräte Ihrer Kontakte, wenn Sie einen <a href="#experiments">Anruf</a> tätigen
|
||||
oder gemeinsam <a href="#webxdc">Apps</a> verwenden.</p>
|
||||
<p>Die verwendeten <a href="#relays">Relays</a> müssen deine IP-Adresse kennen,
|
||||
sowie manchmal auch die Geräte deiner Kontakte, wenn du einen <a href="#calls">Anruf</a> tätigst
|
||||
oder ihr gemeinsam <a href="#webxdc">Apps</a> verwendet.</p>
|
||||
|
||||
<p>IP-Adressen sind für Verbindungen und für Effizienz erforderlich.
|
||||
Sie werden weder gespeichert noch offengelegt.
|
||||
Beachten Sie, dass die IP-Adresse
|
||||
nicht mit einer Adresse, die Sie einem Lieferdienst geben, vergleichbar ist -
|
||||
sondern viel gröber ist und oft nur die Region oder das Land angibt.</p>
|
||||
Sie werden von Delta Chat weder gespeichert noch offengelegt.
|
||||
IP-Adressen sind nicht mit einer Adresse, die du einem Lieferdienst gibst, vergleichbar - sondern viel gröber und oft nur die Stadt oder die Region beschreibend.</p>
|
||||
|
||||
<p>Da dies die Standardfunktion des Internets und anderer Messenger ist,
|
||||
bieten wir hier keine Optionen an und stellen auch keine Fragen im Voraus.</p>
|
||||
|
||||
<p>Wenn Sie Ihre IP-Adresse als Sicherheits- oder Datenschutzrisiko betrachten,
|
||||
empfehlen wir Ihnen, ein VPN in Kombination mit dem System-Lockdown-Modus zu verwenden.
|
||||
Alle einzelnen Apps auf Ihrem System nach IP-Optionen abzusuchen wird nicht zufriedenstellen sein;
|
||||
beispielsweise legt das Antippen eines Links IP-Adressen gegenüber unbekannten Parteien offen und stellt hier das weitaus größere Risiko dar.</p>
|
||||
<p>Wenn du deine IP-Adresse als Risiko betrachtest, empfehlen wir, ein VPN für das gesamte System zu verwenden.
|
||||
Einstellungen auf App-Ebene hinterlassen Lücken überall im System. Wenn man beispielsweise auf einen Link tippt, können IP-Adressen an Unbekannte weitergegeben werden, was bei weitem das größere Risiko darstellt</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
<ul>
|
||||
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
|
||||
@@ -627,6 +642,197 @@ but more than 150 is not recommended.</p>
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1425,23 +1631,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
+422
-222
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,21 @@
|
||||
<li><a href="#zenbat-kide-izan-ditzake-talde-batek">Zenbat kide izan ditzake talde batek?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">Txat barruko aplikazioak</a>
|
||||
<ul>
|
||||
<li><a href="#non-lortu-ditzaket-txat-barruko-aplikazioak">Non lortu ditzaket txat barruko aplikazioak?</a></li>
|
||||
@@ -624,6 +639,197 @@ baina ez da komeni 150 baino gehiago izatea.</p>
|
||||
Delta Chat <a href="#groups">eskubide berberekin</a> txateatzeko aukera ematen duen mezularitza pribatuko zerbitzu bat da.
|
||||
Informazio gehiago nahi baduzu, kontsultatu <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbarren zenbakia</a>.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1431,26 +1637,20 @@ txat-kontaktuak ezin izango dira erraz identifikatu.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Erabiltzen duzun <a href="#relays">erreleak</a> zure IP helbidea jakin behar du,
|
||||
eta, batzuetan, baita zure kontaktuen gailuena ere; adibidez,
|
||||
kontaktu horiekin <a href="#experiments">deiak</a> egiten badituzu edo
|
||||
<a href="#webxdc">aplikazioak</a> erabiltzen badituzu.</p>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP helbideak ezinbestekoak dira konektibitaterako eta eraginkortasunerako.
|
||||
Ez dira gordetzen, ez argitara ematen.
|
||||
Kontuan izan IP helbidea ez dela mezularitzako zerbitzuei-eta eman ohi zaiena
|
||||
bezalako helbide zehatz bat; askoz orokorragoa da, eta askotan eskualdea edo
|
||||
herrialdea baizik ez du identifikatzen.</p>
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>Internetek eta beste mezularitza-aplikazioek horrela funtzionatzen dutenez,
|
||||
ez dugu horren gaineko bestelako aukerarik eskaintzen, ez galderarik egiten.</p>
|
||||
|
||||
<p>Uste baduzu zure IP helbidea jakiteak zure segurtasuna edo pribatutasuna
|
||||
arriskuan jar ditzakeela, zera gomendatuko genizuke, VPN bat erabiltzea,
|
||||
sistema blokeatzeko moduarekin batera. Zure sistemako aplikazio guztietan
|
||||
alternatibak bilatzeak segurtasun-zuloak utziko ditu; adibidez, esteka batean klik egindakoan,
|
||||
IP helbideak agerian geratzen zaizkie hirugarren ezezagunei, eta
|
||||
hori askoz arriskutsuagoa da.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
<ul>
|
||||
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
|
||||
@@ -547,6 +562,197 @@ but more than 150 is not recommended.</p>
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1338,23 +1544,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
<ul>
|
||||
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
|
||||
@@ -627,6 +642,197 @@ but more than 150 is not recommended.</p>
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1425,23 +1631,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#quanti-membri-possono-partecipare-a-un-singolo-gruppo">Quanti membri possono partecipare a un singolo gruppo?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#canali">Canali</a>
|
||||
<ul>
|
||||
<li><a href="#iscriversi-a-un-canale">Iscriversi a un canale</a></li>
|
||||
<li><a href="#creare-un-canale">Creare un canale</a></li>
|
||||
<li><a href="#quanti-iscritti-può-avere-un-canale">Quanti iscritti può avere un canale?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Chiamate</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">Apps in chat</a>
|
||||
<ul>
|
||||
<li><a href="#dove-posso-trovare-le-apps-in-chat">Dove posso trovare le apps in chat?</a></li>
|
||||
@@ -620,6 +635,196 @@ ma non è consigliabile superare i 150.</p>
|
||||
dove Delta Chat è un servizio di messaggistica privato per chattare con <a href="#groups">uguali diritti</a>.
|
||||
Vedi <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">numero di Dunbar</a> per ulteriori approfondimenti.</p>
|
||||
|
||||
<h2 id="canali">
|
||||
|
||||
|
||||
Canali <a href="#canali" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>I canali sono uno strumento da uno a molti per la trasmissione di messaggi.</p>
|
||||
|
||||
<h3 id="iscriversi-a-un-canale">
|
||||
|
||||
|
||||
Iscriversi a un canale <a href="#iscriversi-a-un-canale" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scansiona il <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>Codice QR</strong>
|
||||
oppure tocca il <strong>collegamento di invito</strong> che hai ricevuto dal proprietario del canale.</li>
|
||||
</ul>
|
||||
|
||||
<p>Ecco fatto!
|
||||
Riceverai alcuni dei messaggi dalla cronologia del canale
|
||||
e, da quel momento in poi, tutti i nuovi messaggi dal canale.</p>
|
||||
|
||||
<p><strong>Non preoccuparti,</strong> se non accade immediatamente.
|
||||
Non appena il proprietario del canale si connetterà, la tua richiesta di iscrizione verrà elaborata.</p>
|
||||
|
||||
<p>Poiché tutti i Canali di Delta Chat sono privati e decentralizzati,
|
||||
non esiste una funzione di ricerca pubblica.</p>
|
||||
|
||||
<p>Gli altri iscritti al canale non vedranno che ti sei iscritto e non potranno inviarti messaggi.
|
||||
Il proprietario del canale, tuttavia, potrà inviarti messaggi.
|
||||
Inoltre, vedrà che hai letto un messaggio, a meno che tu non abbia disabilitato le conferme di lettura.</p>
|
||||
|
||||
<p>Se non desideri condividere il tuo profilo principale,
|
||||
puoi anche creare un <a href="#multiple-accounts">profilo dedicato</a> per unirti a un canale.</p>
|
||||
|
||||
<h3 id="creare-un-canale">
|
||||
|
||||
|
||||
Creare un canale <a href="#creare-un-canale" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tocca <strong>Nuova Chat</strong> e scegli <strong>Nuovo Canale</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Inserisci un <strong>nome</strong>, imposta facoltativamente un’<strong>immagine</strong> e una <strong>descrizione</strong>, e fai clic sul pulsante <strong>Crea</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Ora puoi inviare e gestire i messaggi come di consueto.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Dal profilo del canale, <strong>condividi il codice QR o il collegamento di invito con altri</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Gli iscritti riceveranno i tuoi messaggi,
|
||||
ma non potranno inviare messaggi nel tuo canale.
|
||||
Al momento dell’iscrizione, riceveranno <strong>alcuni degli ultimi messaggi della cronologia del canale</strong>.</p>
|
||||
|
||||
<p>Accanto a ciascun messaggio puoi vedere il <strong>numero di visualizzazioni</strong>.
|
||||
Tieni presente che questo conteggio si riferisce solo agli abbonati che hanno attivato le conferme di lettura,
|
||||
quindi il numero reale di visualizzazioni potrebbe essere superiore.</p>
|
||||
|
||||
<h3 id="quanti-iscritti-può-avere-un-canale">
|
||||
|
||||
|
||||
Quanti iscritti può avere un canale? <a href="#quanti-iscritti-può-avere-un-canale" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>I canali sono progettati per un pubblico molto più ampio rispetto ai <a href="#groups">gruppi</a>.</p>
|
||||
|
||||
<p>Il limite pratico dipende dal numero di <a href="#relays">ripetitori</a> utilizzati, quindi non esiste un singolo numero fisso valido ovunque.</p>
|
||||
|
||||
<p>Per canali molto grandi con diverse decine di migliaia di iscritti,
|
||||
consigliamo di utilizzare un <a href="#multiple-accounts">profilo dedicato</a> per il canale
|
||||
e di verificare se il ripetitore è adatto.</p>
|
||||
|
||||
<p>Ma non esitare troppo: Delta Chat è progettato per essere indipendente dal ripetitore,
|
||||
quindi puoi cambiare il tuo ripetitore in qualsiasi momento con facilità -
|
||||
i tuoi iscritti esistenti non se ne accorgeranno nemmeno.
|
||||
In tal caso, dovrai solo aggiornare il collegamento di invito che condividi con i nuovi iscritti.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Chiamate <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1412,23 +1617,20 @@ non possono essere identificati facilmente.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Il <a href="#relays">ripetitore</a> utilizzato deve conoscere il tuo indirizzo IP,
|
||||
e talvolta anche i dispositivi dei tuoi contatti se avete una <a href="#experiments">chiamata</a>
|
||||
o utilizzate <a href="#webxdc">apps</a> insieme.</p>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>Gli indirizzi IP sono necessari per la connettività e l’efficienza.
|
||||
Non sono né persistenti né esposti.
|
||||
Si noti che l’indirizzo IP
|
||||
non è come un indirizzo dettagliato che si fornisce a un servizio di consegna,
|
||||
ma molto più generico, che spesso definisce solo la regione o il paese.</p>
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>Poiché questo è il modo in cui Internet e altri servizi di messaggistica funzionano di default,
|
||||
non offriamo opzioni né poniamo domande in anticipo.</p>
|
||||
|
||||
<p>Se ritieni che il tuo indirizzo IP rappresenti un rischio per la sicurezza o la privacy,
|
||||
ti consigliamo di utilizzare una VPN, in combinazione con la modalità di blocco del sistema.
|
||||
Esplorare le opzioni in tutte le app del tuo sistema lascerà delle lacune.
|
||||
Ad esempio, cliccare su un link espone gli indirizzi IP a sconosciuti e rappresenta il rischio di gran lunga maggiore.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
<ul>
|
||||
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
|
||||
@@ -622,6 +637,197 @@ but more than 150 is not recommended.</p>
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1419,23 +1625,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
+276
-176
@@ -2,31 +2,46 @@
|
||||
<html lang="pl"><head><meta charset="UTF-8" /><meta name="viewport" content="initial-scale=1.0" /><link rel="stylesheet" href="../help.css" /></head><body><ul id="top">
|
||||
<li><a href="#czym-jest-delta-chat">Czym jest Delta Chat?</a>
|
||||
<ul>
|
||||
<li><a href="#howtoe2ee">How can I find people to chat with?</a></li>
|
||||
<li><a href="#why-is-a-chat-marked-as-request">Why is a chat marked as “Request”?</a></li>
|
||||
<li><a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other">How can I put two of my friends in contact with each other?</a></li>
|
||||
<li><a href="#howtoe2ee">Jak znaleźć osoby do czatu?</a></li>
|
||||
<li><a href="#dlaczego-czat-jest-oznaczony-jako-prośba">Dlaczego czat jest oznaczony jako „Prośba”?</a></li>
|
||||
<li><a href="#jak-mogę-skontaktować-ze-sobą-dwóch-znajomych">Jak mogę skontaktować ze sobą dwóch znajomych?</a></li>
|
||||
<li><a href="#multiple-accounts">Czym są profile? Jak mogę przełączać się między nimi?</a></li>
|
||||
<li><a href="#kto-widzi-moje-zdjęcie-profilowe">Kto widzi moje zdjęcie profilowe?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
<li><a href="#signature">Czy w Delta Chat mogę ustawić biografię/status?</a></li>
|
||||
<li><a href="#co-oznacza-przypinanie-wyciszanie-i-archiwizowanie">Co oznacza przypinanie, wyciszanie i archiwizowanie?</a></li>
|
||||
<li><a href="#save">Jak działają „Zapisane wiadomości”?</a></li>
|
||||
<li><a href="#co-oznacza-zielona-kropka">Co oznacza zielona kropka?</a></li>
|
||||
<li><a href="#co-oznaczają-znaczniki-wyświetlane-obok-wiadomości-wychodzących">Co oznaczają znaczniki wyświetlane obok wiadomości wychodzących?</a></li>
|
||||
<li><a href="#edit">Poprawianie literówek i usuwanie wiadomości po wysłaniu</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#mediaquality">Jak obsługiwana jest jakość multimediów?</a></li>
|
||||
<li><a href="#ephemeralmsgs">Jak działają znikające wiadomości?</a></li>
|
||||
<li><a href="#delold">Co się stanie, jeśli włączę opcję „Usuń wiadomości z urządzenia”?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
<li><a href="#remove-account">Jak mogę usunąć swój profil czatu?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#groups">Groups</a>
|
||||
<li><a href="#groups">Grupy</a>
|
||||
<ul>
|
||||
<li><a href="#tworzenie-grupy">Tworzenie grupy</a></li>
|
||||
<li><a href="#addmembers">Add and remove members</a></li>
|
||||
<li><a href="#addmembers">Dodawanie i usuwanie członków</a></li>
|
||||
<li><a href="#usunąłem-się-przez-przypadek">Usunąłem się przez przypadek.</a></li>
|
||||
<li><a href="#nie-chcę-już-otrzymywać-wiadomości-od-grupy">Nie chcę już otrzymywać wiadomości od grupy.</a></li>
|
||||
<li><a href="#cloning-a-group">Cloning a group</a></li>
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
<li><a href="#klonowanie-grupy">Klonowanie grupy</a></li>
|
||||
<li><a href="#ilu-członków-może-należeć-do-jednej-grupy">Ilu członków może należeć do jednej grupy?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Kanały</a>
|
||||
<ul>
|
||||
<li><a href="#subskrybowanie-kanału">Subskrybowanie kanału</a></li>
|
||||
<li><a href="#tworzenie-kanału">Tworzenie kanału</a></li>
|
||||
<li><a href="#ilu-subskrybentów-może-mieć-kanał">Ilu subskrybentów może mieć kanał?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Połączenia</a>
|
||||
<ul>
|
||||
<li><a href="#nawiązywanie-połączenia">Nawiązywanie połączenia</a></li>
|
||||
<li><a href="#odbieranie-lub-odrzucanie-połączenia">Odbieranie lub odrzucanie połączenia</a></li>
|
||||
<li><a href="#w-trakcie-połączenia">W trakcie połączenia</a></li>
|
||||
<li><a href="#nieodebrane-połączenia-i-powiadomienia">Nieodebrane połączenia i powiadomienia</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
@@ -107,87 +122,67 @@
|
||||
|
||||
<p>Delta Chat to niezawodna, zdecentralizowana i bezpieczna aplikacja do błyskawicznego przesyłania wiadomości, dostępna na platformy mobilne i stacjonarne.</p>
|
||||
|
||||
<p>Natychmiastowe tworzenie <strong>prywatnych profili czatu</strong> z bezpiecznymi i interoperacyjnymi <a href="https://chatmail.at/relays">przekaźnikami chatmail</a>, które oferują natychmiastowe dostarczanie wiadomości oraz powiadomienia push dla urządzeń z systemem iOS i Android.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Instant creation of <strong>private chat profiles</strong>
|
||||
with secure and interoperable <a href="https://chatmail.at/relays">chatmail relays</a>
|
||||
that offer instant message delivery, and Push Notifications for iOS and Android devices.</p>
|
||||
<p>Wszechstronna obsługa <a href="#multiple-accounts">wielu profili</a> i <a href="#multiclient">wielu urządzeń</a> na wszystkich platformach i między różnymi <a href="https://chatmail.at/clients">aplikacjami chatmail</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Pervasive <a href="#multiple-accounts">multi-profile</a> and
|
||||
<a href="#multiclient">multi-device</a> support on all platforms
|
||||
and between different <a href="https://chatmail.at/clients">chatmail apps</a>.</p>
|
||||
<p>Interaktywne <a href="#webxdc">aplikacje do czatu</a> w grach i do współpracy</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Interactive <a href="#webxdc">in-chat apps</a> for gaming and collaboration</p>
|
||||
<p><a href="#security-audits">Audytowne szyfrowanie end-to-end</a> zabezpieczające przed atakami sieciowymi i serwerowymi.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="#security-audits">Audited end-to-end encryption</a>
|
||||
safe against network and server attacks.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Free and Open Source software, both app and server side,
|
||||
built on <a href="https://github.com/chatmail/core/blob/main/standards.md#standards-used-in-delta-chat">Internet Standards</a>.</p>
|
||||
<p>Bezpłatne i otwartoźródłowe oprogramowanie zarówno po stronie aplikacji, jak i serwera, stworzone w oparciu o <a href="https://github.com/chatmail/core/blob/main/standards.md#standards-used-in-delta-chat">standardy internetowe</a>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="howtoe2ee">
|
||||
|
||||
|
||||
How can I find people to chat with? <a href="#howtoe2ee" class="anchor"></a>
|
||||
Jak znaleźć osoby do czatu? <a href="#howtoe2ee" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>First, note that Delta Chat is a private messenger.
|
||||
There is no public discovery, <em>you</em> decide about your contacts.</p>
|
||||
<p>Najpierw pamiętaj, że Delta Chat to prywatny komunikator. Nie ma możliwości publicznego wyszukiwania, sam decydujesz o swoich kontaktach.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you are <strong>face to face</strong> with your friend or family,
|
||||
tap the <strong>QR Code</strong> icon <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" />
|
||||
on the main screen.<br />
|
||||
Ask your chat partner to <strong>scan</strong> the QR image
|
||||
with their Delta Chat app.</p>
|
||||
<p>Jeśli jesteś twarzą w twarz ze znajomym lub rodziną, dotknij ikony <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>kodu QR</strong> na ekranie głównym.
|
||||
Poproś partnera czatu o <strong>zeskanowanie</strong> obrazu QR za pomocą aplikacji Delta Chat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For a <strong>remote</strong> contact setup,
|
||||
from the same screen,
|
||||
click “Copy” or “Share” and send the <strong>invite link</strong>
|
||||
through another private chat.</p>
|
||||
<p>Aby skonfigurować kontakt <strong>zdalny</strong>, na tym samym ekranie naciśnij „Kopiuj” lub „Udostępnij” i wyślij <strong>link zaproszenia</strong> za pośrednictwem innego prywatnego czatu.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Now wait while connection gets established.</p>
|
||||
<p>Poczekaj, aż połączenie zostanie nawiązane.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If both sides are online, they will soon see a chat
|
||||
and can start messaging securely.</p>
|
||||
<p>Jeśli obie strony są online, wkrótce zobaczą czat i będą mogły bezpiecznie wysyłać wiadomości.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If one side is offline or in bad network,
|
||||
the ability to chat is delayed until connectivity is restored.</p>
|
||||
<p>Jeśli jedna ze stron jest offline lub ma słaby zasięg, możliwość czatowania zostanie wstrzymana do czasu przywrócenia połączenia.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Congratulations!
|
||||
You now will automatically use <a href="#e2ee">end-to-end encryption</a> with this contact.
|
||||
If you add each other to <a href="#groups">groups</a>, end-to-end encryption will be established among all members.</p>
|
||||
<p>Gratulacje! Teraz będziesz automatycznie korzystać z <a href="#e2ee">szyfrowania typu end-to-end</a> dla tego kontaktu. Jeśli dodacie się nawzajem do <a href="#groups">grup</a>, szyfrowanie typu end-to-end zostanie nawiązane między wszystkimi członkami.</p>
|
||||
|
||||
<h3 id="why-is-a-chat-marked-as-request">
|
||||
<h3 id="dlaczego-czat-jest-oznaczony-jako-prośba">
|
||||
|
||||
|
||||
Why is a chat marked as “Request”? <a href="#why-is-a-chat-marked-as-request" class="anchor"></a>
|
||||
Dlaczego czat jest oznaczony jako „Prośba”? <a href="#dlaczego-czat-jest-oznaczony-jako-prośba" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>As being a private messenger,
|
||||
only friends and family you <a href="#howtoe2ee">share your QR code or invite link with</a> can write to you.</p>
|
||||
<p>Ponieważ jest to prywatny komunikator, tylko znajomi i rodzina, którym <a href="#howtoe2ee">udostępnisz swój kod QR lub link zaproszenia</a>, mogą do ciebie pisać.</p>
|
||||
|
||||
<p>Your friends may share your contact with other friends,
|
||||
this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Request</b></p>
|
||||
<p>Twoi znajomi mogą udostępniać twoje dane kontaktowe innym znajomym, co jest oznaczone jako <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Prośba</b></p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
@@ -201,19 +196,17 @@ this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-s
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="how-can-i-put-two-of-my-friends-in-contact-with-each-other">
|
||||
<h3 id="jak-mogę-skontaktować-ze-sobą-dwóch-znajomych">
|
||||
|
||||
|
||||
How can I put two of my friends in contact with each other? <a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other" class="anchor"></a>
|
||||
Jak mogę skontaktować ze sobą dwóch znajomych? <a href="#jak-mogę-skontaktować-ze-sobą-dwóch-znajomych" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Attach the first contact to the chat of the second using <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attachment Button → Contact</strong>.
|
||||
You can also add a little introduction message.</p>
|
||||
<p>Dołącz pierwszy kontakt do czatu drugiego, używając przycisku <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Dołączania → Kontakt</strong>. Możesz również dodać krótką wiadomość powitalną.</p>
|
||||
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
<p>Drugi kontakt otrzyma wtedy <strong>kartkę</strong> i może ją nacisnąć, aby rozpocząć czat z pierwszym kontaktem.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
@@ -223,15 +216,13 @@ and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>A profile is <strong>a name, a picture</strong> and some additional information for encrypting messages.
|
||||
A profile lives on your device(s) only
|
||||
and uses the server only to relay messages.</p>
|
||||
<p>Profil składa się z <strong>nazwy, zdjęcia</strong> i dodatkowych informacji służących do szyfrowania wiadomości. Profil jest dostępny tylko na twoim urządzeniu (urządzeniach) i korzysta z serwera wyłącznie do przekazywania wiadomości.</p>
|
||||
|
||||
<p>Podczas pierwszej instalacji Delta Chat tworzony jest pierwszy profil.</p>
|
||||
|
||||
<p>Później możesz dotknąć swojego zdjęcia profilowego w lewym górnym rogu, aby <strong>Dodać profile</strong> lub <strong>Przełączyć profile</strong>.</p>
|
||||
|
||||
<p>You may want to use separate profiles for political, family or work related activities.</p>
|
||||
<p>Możesz używać osobnych profili dla aktywności politycznych, rodzinnych lub zawodowych.</p>
|
||||
|
||||
<p>Możesz także dowiedzieć się, <a href="#multiclient">jak używać tego samego profilu na wielu urządzeniach</a>.</p>
|
||||
|
||||
@@ -243,22 +234,19 @@ and uses the server only to relay messages.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Możesz dodać zdjęcie profilowe w swoich ustawieniach. Jeśli napiszesz do swoich kontaktów lub dodasz je za pomocą kodu QR, automatycznie zobaczą je jako Twoje zdjęcie profilowe.</p>
|
||||
<p>Możesz dodać zdjęcie profilowe w swoich ustawieniach. Jeśli napiszesz do swoich kontaktów lub dodasz je za pomocą kodu QR, automatycznie zobaczą je jako twoje zdjęcie profilowe.</p>
|
||||
|
||||
<p>Ze względów prywatności nikt nie widzi Twojego zdjęcia profilowego, dopóki nie napiszesz do niego wiadomości.</p>
|
||||
<p>Ze względów prywatności nikt nie widzi twojego zdjęcia profilowego, dopóki nie napiszesz do niego wiadomości.</p>
|
||||
|
||||
<h3 id="signature">
|
||||
|
||||
|
||||
Can I set a Bio/Status with Delta Chat? <a href="#signature" class="anchor"></a>
|
||||
Czy w Delta Chat mogę ustawić biografię/status? <a href="#signature" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes,
|
||||
you can do so under <strong>Settings → Profile → Bio</strong>.
|
||||
Once you sent a message to a contact,
|
||||
they will see it when they view your contact details.</p>
|
||||
<p>Tak, możesz to zrobić w <strong>Ustawieniach → Profil → Biografia</strong>. Po wysłaniu wiadomości do kontaktu zostanie ona wyświetlona, gdy będzie on przeglądał twoje dane kontaktowe.</p>
|
||||
|
||||
<h3 id="co-oznacza-przypinanie-wyciszanie-i-archiwizowanie">
|
||||
|
||||
@@ -278,7 +266,7 @@ they will see it when they view your contact details.</p>
|
||||
<p><strong>Wycisz czaty</strong>, jeśli nie chcesz otrzymywać z nich powiadomień. Wyciszone czaty pozostają na swoim miejscu i możesz też przypiąć wyciszony czat.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Archiwizuj czaty</strong>, jeśli nie chcesz ich już widzieć na liście czatów. Zarchiwizowane czaty pozostają dostępne nad listą czatów lub poprzez wyszukiwanie.</p>
|
||||
<p><strong>Archiwizuj czaty</strong>, jeśli nie chcesz ich już widzieć na liście czatów. Pozostają dostępne nad listą czatów lub poprzez wyszukiwanie i są oznaczone jako <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Zarchiwizowane</b></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Gdy zarchiwizowany czat otrzyma nową wiadomość, o ile nie zostanie wyciszony, <strong>wyskoczy z archiwum</strong> i wróci na twoją listę czatów.
|
||||
@@ -309,7 +297,7 @@ they will see it when they view your contact details.</p>
|
||||
<p>Później otwórz czat „Zapisane wiadomości” — zobaczysz tam zapisane wiadomości. Naciskając <img style="vertical-align:middle; width:1.2em; margin:1px" src="../go-to-original.png" alt="ikona strzałki w prawo" />, możesz wrócić do oryginalnej wiadomości w oryginalnym czacie</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Na koniec możesz również użyć „Zapisz wiadomości”, aby robić <strong>osobiste notatki</strong> — otwórz czat, wpisz coś, dodaj zdjęcie lub wiadomość głosową itp.</p>
|
||||
<p>Na koniec możesz również użyć „Zapisanych wiadomości”, aby robić <strong>osobiste notatki</strong> — otwórz czat, wpisz coś, dodaj zdjęcie lub wiadomość głosową itp.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Ponieważ „Zapisane wiadomości” są zsynchronizowane, mogą być bardzo przydatne do przesyłania danych między urządzeniami</p>
|
||||
@@ -326,13 +314,9 @@ they will see it when they view your contact details.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>You can sometimes see a <strong>green dot</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />
|
||||
next to the avatar of a contact.
|
||||
It means they were <strong>recently seen by you</strong> in the last 10 minutes,
|
||||
e.g. because they messaged you or sent a read receipt.</p>
|
||||
<p>Czasami można zobaczyć <strong>zieloną kropkę</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" /> obok awatara kontaktu. Oznacza to, że był on <strong>niedawno widziany przez ciebie</strong> w ciągu ostatnich 10 minut, np. wysłał ci wiadomość lub potwierdzenie odczytu.</p>
|
||||
|
||||
<p>So this is not a real time online status
|
||||
and others will as well not always see that you are “online”.</p>
|
||||
<p>Nie jest to więc status online w czasie rzeczywistym i inni również nie zawsze zobaczą, że jesteś „online”.</p>
|
||||
|
||||
<h3 id="co-oznaczają-znaczniki-wyświetlane-obok-wiadomości-wychodzących">
|
||||
|
||||
@@ -344,19 +328,16 @@ and others will as well not always see that you are “online”.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong>One tick</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick1.png" alt="" />
|
||||
means that the message was sent successfully to the <a href="#relays">relay</a>.</p>
|
||||
<p><strong>Jeden znacznik</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick1.png" alt="" /> oznacza, że wiadomość została pomyślnie wysłana do <a href="#relays">przekaźnika</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Two ticks</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick2.png" alt="" />
|
||||
indicate your contact has read the message.</p>
|
||||
<p><strong>Dwa znaczniki</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick2.png" alt="" /> oznaczają, że twój kontakt przeczytał wiadomość.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>In <a href="#groups">groups</a> the second tick means that at least one member has reported back having read the message.</p>
|
||||
<p>W <a href="#groups">grupach</a> drugi znacznik oznacza, że co najmniej jeden członek potwierdził przeczytanie wiadomości.</p>
|
||||
|
||||
<p>You will only get the second tick if both you and one of the recipients who read the message
|
||||
has <strong>Settings → Chats → Read Receipts</strong> enabled.</p>
|
||||
<p>Drugi znacznik pojawi się tylko wtedy, gdy ty i jeden z odbiorców, którzy przeczytali wiadomość, macie włączoną opcję <strong>Ustawienia → Czaty → Potwierdzenie odczytu</strong>.</p>
|
||||
|
||||
<h3 id="edit">
|
||||
|
||||
@@ -382,26 +363,22 @@ has <strong>Settings → Chats → Read Receipts</strong> enabled.</p>
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <a href="#mediaquality" class="anchor"></a>
|
||||
Jak obsługiwana jest jakość multimediów? <a href="#mediaquality" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Images, videos, files, voice messages etc. can be sent using the <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attach-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons.</p>
|
||||
<p>Obrazy, filmy, pliki, wiadomości głosowe itp. można wysyłać za pomocą przycisków: <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Załącz</strong> lub <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /><strong>Wiadomość głosowa</strong>.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>By default, compression ensures <strong>fast, efficient delivery</strong> that respects everyone’s data limits and storage.
|
||||
This is ideal for everyday communication.</p>
|
||||
<p>Domyślnie kompresja zapewnia <strong>szybką i wydajną dostawę</strong>, respektując limity danych i pamięci wszystkich użytkowników. Jest to idealne rozwiązanie do codziennej komunikacji.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</strong>.</p>
|
||||
<p>W regionach o słabszej łączności można wybrać wyższą kompresję w <strong>Ustawieniach → Czaty → Jakość mediów wychodzących</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you specifically need to send media in its <strong>original quality</strong>, use <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attach → File</strong> in the chat.
|
||||
Please use this method sparingly, as sending original files will significantly increase data usage for you and all recipients in the chat.</p>
|
||||
<p>Jeśli chcesz wysłać multimedia w <strong>oryginalnej jakości</strong>, użyj w czacie opcji <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Załącz → Plik</strong>. Używaj tej metody oszczędnie, ponieważ wysyłanie oryginalnych plików znacznie zwiększy zużycie danych przez ciebie i wszystkich odbiorców na czacie.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -415,21 +392,11 @@ Please use this method sparingly, as sending original files will significantly i
|
||||
|
||||
<p>Możesz włączyć „znikające wiadomości” w ustawieniach czatu, w prawym górnym rogu okna czatu, wybierając przedział czasu od 5 minut do 1 roku.</p>
|
||||
|
||||
<p>Until the setting is turned off again,
|
||||
each chat member’s Delta Chat app takes care
|
||||
of deleting the messages
|
||||
after the selected time span.
|
||||
The time span begins
|
||||
when the receiver first sees the message in Delta Chat.
|
||||
The messages are deleted both,
|
||||
on the servers,
|
||||
and in the apps itself.</p>
|
||||
<p>Dopóki ustawienie nie zostanie ponownie wyłączone, aplikacja Delta Chat u każdego członka czatu zajmie się usuwaniem wiadomości po wybranym okresie. Przedział czasu rozpoczyna się w momencie, gdy odbiorca po raz pierwszy zobaczy wiadomość w Delta Chat. Wiadomości są usuwane zarówno na serwerze, jak i w samej aplikacji.</p>
|
||||
|
||||
<p>Pamiętaj, że na znikających wiadomościach możesz polegać tylko wtedy, gdy ufasz swoim partnerom czatu; złośliwi partnerzy czatu mogą robić zdjęcia lub w inny sposób zapisywać, kopiować lub przesyłać dalej wiadomości przed usunięciem.</p>
|
||||
|
||||
<p>Apart from that,
|
||||
if one chat partner uninstalls Delta Chat,
|
||||
the (anyway encrypted) messages may take longer to get deleted from their server.</p>
|
||||
<p>Poza tym, jeśli jeden z uczestników czatu odinstaluje aplikację Delta Chat, usunięcie (i tak zaszyfrowanych) wiadomości z jego serwera może potrwać dłużej.</p>
|
||||
|
||||
<h3 id="delold">
|
||||
|
||||
@@ -439,46 +406,35 @@ the (anyway encrypted) messages may take longer to get deleted from their server
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Jeśli chcesz zaoszczędzić miejsce na urządzeniu, możesz wybrać opcję automatycznego usuwania starych wiadomości.</p>
|
||||
<p>Jeśli chcesz zaoszczędzić miejsce na swoim urządzeniu, możesz wybrać opcję automatycznego usuwania starych wiadomości.</p>
|
||||
|
||||
<p>Aby ją włączyć, przejdź do „Usuń wiadomości z urządzenia” w ustawieniach w sekcji „Czaty i media”. Możesz ustawić przedział czasowy pomiędzy „po 1 godzinie” a „po 1 roku”; w ten sposób <em>wszystkie</em> wiadomości zostaną usunięte z urządzenia, gdy tylko staną się starsze.</p>
|
||||
<p>Aby ją włączyć, przejdź do <strong>Ustawienia → Czaty → Usuń wiadomości z urządzenia</strong> . Możesz ustawić przedział czasowy pomiędzy „po 1 godzinie” a „po 1 roku”; w ten sposób <em>wszystkie</em> wiadomości zostaną usunięte z urządzenia, gdy tylko staną się starsze.</p>
|
||||
|
||||
<h3 id="remove-account">
|
||||
|
||||
|
||||
How can I delete my chat profile? <a href="#remove-account" class="anchor"></a>
|
||||
Jak mogę usunąć swój profil czatu? <a href="#remove-account" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>If you are using more than one chat profile,
|
||||
you can remove single ones in the top profile switcher menu (on Android and iOS),
|
||||
or in the sidebar with a right click (in the Desktop app).
|
||||
Chat profiles are only removed on the device where deletion was triggered.
|
||||
Chat profiles on other devices will continue to fully function.</p>
|
||||
<p>Jeśli używasz więcej niż jednego profilu czatu, możesz usunąć pojedyncze profile w górnym menu przełączania profili (na Androidzie i iOS) lub w pasku bocznym, klikając prawym przyciskiem myszy (w aplikacji na komputery). Profile czatu są usuwane tylko na urządzeniu, na którym nastąpiło usunięcie. Profile czatu na innych urządzeniach będą nadal w pełni działać.</p>
|
||||
|
||||
<p>If you use a single default chat profile you can simply uninstall the app.
|
||||
This will still automatically trigger deletion of all associated address data on the chatmail server.
|
||||
For more info, please refer to <a href="https://nine.testrun.org/info.html#account-deletion">nine.testrun.org address-deletion</a>
|
||||
or the respective page from your chosen <a href="https://chatmail.at/relays">3rd party chatmail server</a>.</p>
|
||||
<p>Jeśli używasz jednego domyślnego profilu czatu, możesz po prostu odinstalować aplikację. Spowoduje to automatyczne usunięcie wszystkich powiązanych danych adresowych na serwerze czatu. Aby uzyskać więcej informacji, zapoznaj się z informacjami o <a href="https://nine.testrun.org/info.html#account-deletion">usuwaniu adresów na stronie nine.testrun.org</a> lub odpowiednią stroną wybranego <a href="https://chatmail.at/relays">serwera czatu innej firmy</a>.</p>
|
||||
|
||||
<h2 id="groups">
|
||||
|
||||
|
||||
Groups <a href="#groups" class="anchor"></a>
|
||||
Grupy <a href="#groups" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Groups let several people chat together privately with <strong>equal rights</strong>.</p>
|
||||
<p>Grupy pozwalają kilku osobom na prywatną rozmowę na <strong>równych prawach</strong>.</p>
|
||||
|
||||
<p>Anyone can
|
||||
change the group name or avatar,
|
||||
<a href="#addmembers">add or remove members</a>,
|
||||
set <a href="#ephemeralmsgs">disappearing messages</a>,
|
||||
and <a href="#edit">delete their own messages</a> from all member’s devices.</p>
|
||||
<p>Każdy może zmienić nazwę grupy lub awatar, <a href="#addmembers">dodawać lub usuwać członków</a>, ustawiać <a href="#ephemeralmsgs">znikające wiadomości</a> oraz <a href="#edit">usuwać własne wiadomości</a> z urządzeń wszystkich członków.</p>
|
||||
|
||||
<p>Because all members have the same rights, groups work best among <strong>trusted friends and family</strong>.</p>
|
||||
<p>Ponieważ wszyscy członkowie mają te same uprawnienia, grupy najlepiej sprawdzają się w gronie <strong>zaufanych przyjaciół i rodziny</strong>.</p>
|
||||
|
||||
<h3 id="tworzenie-grupy">
|
||||
|
||||
@@ -493,43 +449,37 @@ and <a href="#edit">delete their own messages</a> from all member’s devices.</
|
||||
<p>Wybierz <strong>Nowy czat</strong>, a następnie <strong>Nowa grupa</strong> z menu w prawym górnym rogu lub naciśnij odpowiedni przycisk na Androidzie / iOS.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Na następnym ekranie wybierz <strong>członków grupy</strong> i zdefiniuj <strong>nazwę grupy</strong>. Możesz też wybrać awatar <strong>grupy</strong>.</p>
|
||||
<p>Na następnym ekranie wybierz <strong>członków grupy</strong> i zdefiniuj <strong>nazwę grupy</strong>. Możesz też wybrać <strong>awatar grupy</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Zaraz po napisaniu pierwszej wiadomości w grupie wszyscy członkowie zostaną poinformowani o nowej grupie i mogą odpowiedzieć w grupie (jeżeli nie napiszesz wiadomości w grupie, grupa jest niewidoczna dla członków).</p>
|
||||
<p>Gdy tylko napiszesz <strong>pierwszą wiadomość</strong> w grupie, wszyscy członkowie zostaną poinformowani o nowej grupie i będą mogli odpowiadać w grupie (dopóki nie napiszesz wiadomości w grupie, grupa będzie niewidoczna dla członków).</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="addmembers">
|
||||
|
||||
|
||||
Add and remove members <a href="#addmembers" class="anchor"></a>
|
||||
Dodawanie i usuwanie członków <a href="#addmembers" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>All group members have the <strong>same rights</strong>.
|
||||
For this reason, everyone can delete any member or add new ones.</p>
|
||||
<p>Wszyscy członkowie grupy mają <strong>takie same uprawnienia</strong>. Z tego powodu każdy może usunąć dowolnego członka lub dodać nowych.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>To <strong>add or delete members</strong>, tap the group name in the chat and select the member to add or remove.</p>
|
||||
<p>Aby <strong>dodać lub usunąć członków</strong>, dotknij nazwę grupy na czacie i wybierz członka, którego chcesz dodać lub usunąć.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If the member is not yet in your contact list, but <strong>face to face</strong> with you,
|
||||
from the same screen, show a <strong>QR code</strong>.<br />
|
||||
Ask your chat partner to <strong>scan</strong> the QR image with their Delta Chat app by tapping
|
||||
<img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> on the main screen.</p>
|
||||
<p>Jeśli członek nie znajduje się jeszcze na twojej liście kontaktów, ale rozmawiasz z nim <strong>twarzą w twarz</strong>, na tym samym ekranie pokaż mu <strong>kod QR</strong>.
|
||||
Poproś partnera czatu o <strong>zeskanowanie</strong> obrazu QR za pomocą aplikacji Delta Chat, dotykając <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> na ekranie głównym.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For a <strong>remote</strong> member addition,
|
||||
click “Copy” or “Share” and send the <strong>invite link</strong>
|
||||
through another private chat to the new member.</p>
|
||||
<p>Aby dodać członka <strong>zdalnie</strong>, naciśnij „Kopiuj” lub „Udostępnij” i wyślij <strong>link zaproszenia</strong> nowemu członkowi za pośrednictwem innego prywatnego czatu.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>QR code and invite link can be used to add several members.
|
||||
However, since groups are <a href="#groups">meant for trusted people</a>, avoid sharing them publicly.</p>
|
||||
<p>Kod QR i link zaproszenia można wykorzystać do dodania kilku członków. Ponieważ jednak grupy są <a href="#groups">przeznaczone dla zaufanych osób</a>, unikaj udostępniania ich publicznie.</p>
|
||||
|
||||
<h3 id="usunąłem-się-przez-przypadek">
|
||||
|
||||
@@ -539,8 +489,7 @@ However, since groups are <a href="#groups">meant for trusted people</a>, avoid
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Ponieważ nie jesteś członkiem grupy, nie możesz dodać siebie ponownie.
|
||||
Jednak nie ma problemu, po prostu poproś dowolnego członka grupy na normalnym czacie, aby dodał cię ponownie.</p>
|
||||
<p>Ponieważ nie jesteś członkiem grupy, nie możesz dodać siebie ponownie. Jednak nie ma problemu, po prostu poproś dowolnego członka grupy na normalnym czacie, aby dodał cię ponownie.</p>
|
||||
|
||||
<h3 id="nie-chcę-już-otrzymywać-wiadomości-od-grupy">
|
||||
|
||||
@@ -555,47 +504,203 @@ However, since groups are <a href="#groups">meant for trusted people</a>, avoid
|
||||
Jeśli później będziesz chciał ponownie dołączyć do grupy, poproś innego członka grupy, aby dodał cię do grupy.</li>
|
||||
</ul>
|
||||
|
||||
<p>Alternatywnie możesz też „Wyłączyć powiadomienia” dla grupy dzięki temu otrzymasz wszystkie wiadomości i
|
||||
nadal będziesz mógł pisać, ale nie będziesz już powiadamiany o żadnych nowych wiadomościach.</p>
|
||||
<p>Alternatywnie możesz też „Wyłączyć powiadomienia” dla grupy, dzięki temu otrzymasz wszystkie wiadomości i nadal będziesz mógł pisać, ale nie będziesz już powiadamiany o żadnych nowych wiadomościach.</p>
|
||||
|
||||
<h3 id="cloning-a-group">
|
||||
<h3 id="klonowanie-grupy">
|
||||
|
||||
|
||||
Cloning a group <a href="#cloning-a-group" class="anchor"></a>
|
||||
Klonowanie grupy <a href="#klonowanie-grupy" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>You can duplicate a group to start a separate discussion
|
||||
or to exclude members without them noticing.</p>
|
||||
<p>Możesz zduplikować grupę, aby rozpocząć osobną dyskusję lub wykluczyć członków bez ich wiedzy.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Open the group profile and tap <strong>Clone Chat</strong> (Android/iOS),
|
||||
or right-click the group in the chat list (Desktop).</p>
|
||||
<p>Otwórz profil grupy i dotknij opcji <strong>Klonuj czat</strong> (Android/iOS) lub kliknij prawym przyciskiem myszy grupę na liście czatów (komputer).</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Set a new name, choose an avatar, and adjust the member list if needed.</p>
|
||||
<p>Ustaw nową nazwę, wybierz awatar i w razie potrzeby dostosuj listę członków.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>The new group is <strong>fully independent</strong> from the original,
|
||||
which continues to work as before.</p>
|
||||
<p>Nowa grupa jest <strong>w pełni niezależna</strong> od oryginalnej, która nadal działa jak dotychczas.</p>
|
||||
|
||||
<h3 id="how-many-members-can-participate-in-a-single-group">
|
||||
<h3 id="ilu-członków-może-należeć-do-jednej-grupy">
|
||||
|
||||
|
||||
How many members can participate in a single group? <a href="#how-many-members-can-participate-in-a-single-group" class="anchor"></a>
|
||||
Ilu członków może należeć do jednej grupy? <a href="#ilu-członków-może-należeć-do-jednej-grupy" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>There is no strict technical limit,
|
||||
but more than 150 is not recommended.</p>
|
||||
<p>Nie ma ścisłego limitu technicznego, ale nie zaleca się przekraczania 150 osób.</p>
|
||||
|
||||
<p>As groups get larger, they can become socially unstable and may need a hierarchy -
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
<p>W miarę jak grupy się rozrastają, mogą stawać się niestabilne społecznie i wymagać hierarchii – gdzie Delta Chat pełni rolę prywatnego komunikatora do czatowania na <a href="#groups">równych prawach</a>. Więcej informacji znajdziesz w artykule <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Liczba Dunbara</a>.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Kanały <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Kanały to narzędzie typu jeden do wielu, służące do nadawania wiadomości.</p>
|
||||
|
||||
<h3 id="subskrybowanie-kanału">
|
||||
|
||||
|
||||
Subskrybowanie kanału <a href="#subskrybowanie-kanału" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Zeskanuj <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>kod QR</strong> lub naciśnij link zaproszenia otrzymany od właściciela kanału.</li>
|
||||
</ul>
|
||||
|
||||
<p>To wszystko! Otrzymasz kilka wiadomości z historii kanału, a od tego momentu wszystkie nowe wiadomości z kanału.</p>
|
||||
|
||||
<p><strong>Nie martw się</strong>, jeśli to nie nastąpi od razu. Gdy właściciel kanału będzie online, twoja prośba o dołączenie zostanie przetworzona.</p>
|
||||
|
||||
<p>Podobnie jak cała platforma Delta Chat, również kanały są prywatne i zdecentralizowane, dlatego nie ma możliwości publicznego ujawnienia.</p>
|
||||
|
||||
<p>Inni subskrybenci kanału nie zobaczą, że go subskrybujesz i nie będą mogli wysyłać ci wiadomości. Właściciel kanału może jednak napisać do ciebie. Zobaczy również, że przeczytałeś wiadomość, chyba że masz wyłączone potwierdzenia odczytu.</p>
|
||||
|
||||
<p>Jeśli nie chcesz udostępniać swojego głównego profilu, możesz również utworzyć <a href="#multiple-accounts">dedykowany profil</a> do dołączenia do kanału.</p>
|
||||
|
||||
<h3 id="tworzenie-kanału">
|
||||
|
||||
|
||||
Tworzenie kanału <a href="#tworzenie-kanału" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Naciśnij <strong>Nowy cza</strong> i wybierz <strong>Nowy kanał</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Wprowadź <strong>nazwę</strong>, opcjonalnie ustaw <strong>obraz</strong> i <strong>opis</strong>, a następnie naciśnij przycisk <strong>Utwórz</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Możesz teraz wysyłać i zarządzać wiadomościami jak zwykle.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Z profilu kanału <strong>udostępnij kod QR lub link zaproszenia innym osobom</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subskrybenci będą otrzymywać twoje wiadomości, ale nie będą mogli wysyłać wiadomości na twoim kanale. Po zasubskrybowaniu otrzymają <strong>kilka najnowszych wiadomości z historii kanału</strong>.</p>
|
||||
|
||||
<p>Obok każdej wiadomości jest widoczna <strong>liczba wyświetleń</strong>. Pamiętaj, że uwzględnia ona tylko subskrybentów z włączonymi potwierdzeniami odczytu, więc rzeczywista liczba wyświetleń może być wyższa.</p>
|
||||
|
||||
<h3 id="ilu-subskrybentów-może-mieć-kanał">
|
||||
|
||||
|
||||
Ilu subskrybentów może mieć kanał? <a href="#ilu-subskrybentów-może-mieć-kanał" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Kanały są przeznaczone dla znacznie większej publiczności niż <a href="#groups">grupy</a>.</p>
|
||||
|
||||
<p>Praktyczny limit zależy od używanego <a href="#relays">przekaźnika</a>, więc nie ma jednej, stałej liczby, która obowiązywałaby wszędzie.</p>
|
||||
|
||||
<p>W przypadku naprawdę dużych kanałów z dziesiątkami tysięcy subskrybentów zalecamy użycie <a href="#multiple-accounts">dedykowanego profilu</a> dla kanału i sprawdzenie, czy przekaźnik jest odpowiedni.</p>
|
||||
|
||||
<p>Ale nie wahaj się zbytnio: Delta Chat został zaprojektowany tak, aby nie był zależny od przekaźnika, więc możesz go łatwo zmienić w dowolnym momencie – twoi obecni subskrybenci nawet tego nie zauważą. W takim przypadku wystarczy zaktualizować link zaproszenia udostępniany nowym subskrybentom.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Połączenia <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat umożliwia indywidualne <strong>połączenia audio i wideo</strong>.</p>
|
||||
|
||||
<p>Połączenia są obsługiwane na komputerach stacjonarnych, Ubuntu Touch, iOS oraz Androidzie 8 i nowszych.</p>
|
||||
|
||||
<h3 id="nawiązywanie-połączenia">
|
||||
|
||||
|
||||
Nawiązywanie połączenia <a href="#nawiązywanie-połączenia" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>W czacie indywidualnym dotknij <strong>ikony połączenia</strong> 📞.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Otworzy się małe menu, w którym możesz wybrać, czy chcesz nawiązać <strong>połączenie audio</strong>, czy <strong>wideo</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="odbieranie-lub-odrzucanie-połączenia">
|
||||
|
||||
|
||||
Odbieranie lub odrzucanie połączenia <a href="#odbieranie-lub-odrzucanie-połączenia" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Gdy ktoś do ciebie dzwoni, Delta Chat wyświetla <strong>ekran połączenia przychodzącego</strong> lub powiadomienie.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Dotknij <strong>Akceptuj</strong>, aby odebrać, lub <strong>Odrzuć</strong>, aby odrzucić połączenie.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="w-trakcie-połączenia">
|
||||
|
||||
|
||||
W trakcie połączenia <a href="#w-trakcie-połączenia" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Możesz <strong>wyciszyć</strong> mikrofon.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Możesz <strong>włączyć lub wyłączyć kamerę</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Na urządzeniach mobilnych możesz <strong>przełączać się między przednią i tylną kamerą</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>W zależności od urządzenia możesz również wybrać wyjście audio lub skorzystać z trybu obrazu w obrazie. Na komputerach stacjonarnych połączenie jest wyświetlane w dedykowanym oknie i możesz kontynuować korzystanie z głównego okna Delta Chat jak zwykle.</p>
|
||||
|
||||
<h3 id="nieodebrane-połączenia-i-powiadomienia">
|
||||
|
||||
|
||||
Nieodebrane połączenia i powiadomienia <a href="#nieodebrane-połączenia-i-powiadomienia" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Jeśli nie odbierzesz, nie usłyszysz dzwonka lub nie będziesz mieć urządzenia pod ręką, połączenie zostanie oznaczone jako <strong>nieodebrane</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Tylko zaakceptowane przez ciebie kontakty</strong> mogą wywołać dzwonek na urządzeniu. Prośby o kontakt będą wyświetlane normalnie i nie będą dzwonić.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>W <strong>Ustawienia → Powiadomienia → Połączenia</strong> możesz całkowicie wyłączyć specjalny ekran dzwonka. Jeśli to zrobisz, nie będzie ci przeszkadzał żaden dzwonek, a połączenie nadal będzie można odebrać, dotykając w czacie dymku wiadomości o przychodzącym połączeniu.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
@@ -849,8 +954,7 @@ Welcome to the power of the interoperable chatmail relay network :)</p>
|
||||
<p>Sprawdź dokładnie, czy oba urządzenia są w tym <strong>samym Wi-Fi lub tej samej sieci</strong></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Na <strong>Windowsie</strong>, przejdź do “Panel sterowania / Sieć i internet” i upewnij się, że <strong>Sieć prywatna</strong> jest wybrana jako “Typ profilu sieci”
|
||||
(po przeniesieniu możesz wrócić do pierwotnej wartości)</p>
|
||||
<p>Na <strong>Windowsie</strong>, przejdź do „Panel sterowania / Sieć i internet” i upewnij się, że <strong>Sieć prywatna</strong> jest wybrana jako „Typ profilu sieci” (po przeniesieniu możesz wrócić do pierwotnej wartości)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>W systemie <strong>iOS</strong> upewnij się, że jest przydzielony dostęp do opcji „Ustawienia » Aplikacje » Delta Chat » <strong>Sieć lokalna</strong>”</p>
|
||||
@@ -894,15 +998,14 @@ Welcome to the power of the interoperable chatmail relay network :)</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Na starym urządzeniu przejdź do <strong>Ustawienia → Czaty i media → Eksport kopii zapasowej</strong>. Wprowadź swój PIN odblokowania ekranu, wzór lub hasło. Następnie możesz nacisnąć „Utwórz kopię”. Spowoduje to zapisanie pliku kopii zapasowej na urządzeniu. Teraz musisz jakoś przenieść go na inne urządzenie.</p>
|
||||
<p>Na starym urządzeniu przejdź do <strong>Ustawienia → Czaty → Eksport kopii zapasowej</strong>. Wprowadź swój PIN odblokowania ekranu, wzór lub hasło. Następnie możesz nacisnąć „Utwórz kopię”. Spowoduje to zapisanie pliku kopii zapasowej na urządzeniu. Teraz musisz jakoś przenieść go na inne urządzenie.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Na nowym urządzeniu, na ekranie logowania, zamiast logować się na swoje konto e-mail, wybierz <strong>Przywróć z kopii zapasowej</strong>. Po zaimportowaniu Twoje rozmowy, klucze szyfrujące i multimedia powinny zostać skopiowane na nowe urządzenie.
|
||||
Jeśli korzystasz z iOS i napotykasz trudności, może <a href="https://support.delta.chat/t/import-backup-to-ios/1628">ten poradnik</a> Ci pomoże.</p>
|
||||
<p>Na nowym urządzeniu wybierz: <strong>Mam już profil → Przywróć z kopii zapasowej</strong>. Jeśli korzystasz z iOS i napotykasz trudności, może <a href="https://support.delta.chat/t/import-backup-to-ios/1628">ten poradnik</a> ci pomoże.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Jesteś teraz zsynchronizowany i możesz używać obu urządzeń do wysyłania i odbierania wiadomości zaszyfrowanych end-to-end w komunikacji ze swoimi partnerami.</p>
|
||||
<p>Jesteś teraz zsynchronizowany i w komunikacji ze swoimi partnerami możesz używać obu urządzeń do wysyłania i odbierania wiadomości zaszyfrowanych metodą end-to-end.</p>
|
||||
|
||||
<h3 id="czy-są-jakieś-plany-wprowadzenia-klienta-web-delta-chat">
|
||||
|
||||
@@ -1289,23 +1392,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#quantos-membros-podem-participar-em-um-único-grupo">Quantos membros podem participar em um único grupo?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">Aplicativos embutidos</a>
|
||||
<ul>
|
||||
<li><a href="#onde-posso-obter-aplicativos-embutidos">Onde posso obter aplicativos embutidos?</a></li>
|
||||
@@ -622,6 +637,197 @@ mas não é recomendável mais de 150.</p>
|
||||
mas o Delta Chat é um mensageiro privado para conversar com <a href="#groups">direitos iguais</a>.
|
||||
Consulte o <a href="https://pt.wikipedia.org/wiki/N%C3%BAmero_de_Dunbar">Número de Dunbar</a> para obter mais informações.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1420,23 +1626,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#сколько-участников-может-быть-в-одной-группе">Сколько участников может быть в одной группе?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Каналы</a>
|
||||
<ul>
|
||||
<li><a href="#подписка-на-канал">Подписка на канал</a></li>
|
||||
<li><a href="#создание-канала">Создание канала</a></li>
|
||||
<li><a href="#какое-максимальное-количество-подписчиков-может-быть-у-канала">Какое максимальное количество подписчиков может быть у канала?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Звонки</a>
|
||||
<ul>
|
||||
<li><a href="#как-сделать-звонок">Как сделать звонок</a></li>
|
||||
<li><a href="#принять-или-отклонить-вызов">Принять или отклонить вызов</a></li>
|
||||
<li><a href="#во-время-звонка">Во время звонка</a></li>
|
||||
<li><a href="#пропущенные-вызовы-и-уведомления">Пропущенные вызовы и уведомления</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">Встроенные приложения чата</a>
|
||||
<ul>
|
||||
<li><a href="#где-можно-найти-встроенные-приложения">Где можно найти встроенные приложения?</a></li>
|
||||
@@ -622,6 +637,197 @@
|
||||
в то время как Delta Chat - это приватный мессенджер для общения на <a href="#groups">равных правах</a>.
|
||||
Смотрите <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">число Данбара</a> для более глубокого понимания.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Каналы <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Каналы представляют собой инструмент типа “один-ко-многим” для трансляции сообщений.</p>
|
||||
|
||||
<h3 id="подписка-на-канал">
|
||||
|
||||
|
||||
Подписка на канал <a href="#подписка-на-канал" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Отсканируйте <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR-код</strong>
|
||||
или нажмите на ссылку-приглашение, которую вы получили от владельца канала.</li>
|
||||
</ul>
|
||||
|
||||
<p>Всё готово!
|
||||
Сначала вы получите несколько сообщений из истории канала,
|
||||
а затем — все новые сообщения, поступающие в него.</p>
|
||||
|
||||
<p><strong>Не беспокойтесь,</strong> если это произойдет не сразу.
|
||||
Как только владелец канала выйдет в сеть, ваш запрос на вступление будет обработан.</p>
|
||||
|
||||
<p>Как и весь Delta Chat, каналы являются приватными и децентрализованными,
|
||||
поэтому возможность публичного поиска каналов отсутствует.</p>
|
||||
|
||||
<p>Другие подписчики канала не увидят факта вашей подписки и не смогут отправлять вам сообщения.
|
||||
Однако, владелец канала сможет отправить вам сообщение.
|
||||
Также он будет видеть, что вы прочитали сообщение, если только вы не отключите подтверждение о прочтении.</p>
|
||||
|
||||
<p>Если вы не хотите использовать свой основной профиль,
|
||||
вы можете создать <a href="#multiple-accounts">специальный профиль</a> для подписки на канал.</p>
|
||||
|
||||
<h3 id="создание-канала">
|
||||
|
||||
|
||||
Создание канала <a href="#создание-канала" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Нажмите <strong>Новый чат</strong> и выберите <strong>Новый канал</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Введите <strong>название</strong>, по желанию добавьте <strong>изображение</strong> и <strong>описание</strong>, а затем нажмите кнопку <strong>Создать</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Теперь вы можете отправлять сообщения и управлять ими в обычном режиме.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В профиле канала вы можете <strong>поделиться QR-кодом или ссылкой-приглашением с другими пользователями</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Подписчики будут получать ваши сообщения,
|
||||
но не смогут отправлять сообщения в вашем канале.
|
||||
При подписке они получат <strong>несколько последних сообщений из истории канала</strong>.</p>
|
||||
|
||||
<p>Рядом с каждым сообщением вы можете увидеть <strong>количество просмотров</strong>.
|
||||
Обратите внимание, что учитываются только те подписчики, у которых включены уведомления о прочтении,
|
||||
поэтому реальное количество просмотров может быть больше.</p>
|
||||
|
||||
<h3 id="какое-максимальное-количество-подписчиков-может-быть-у-канала">
|
||||
|
||||
|
||||
Какое максимальное количество подписчиков может быть у канала? <a href="#какое-максимальное-количество-подписчиков-может-быть-у-канала" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Каналы предназначены для гораздо более широкой аудитории, чем <a href="#groups">группы</a>.</p>
|
||||
|
||||
<p>Практический предел зависит от используемого <a href="#relays">релея</a>,
|
||||
поэтому не существует единого фиксированного значения, применимого во всех случаях.</p>
|
||||
|
||||
<p>Для крайне крупных каналов с десятками тысяч подписчиков,
|
||||
мы рекомендуем использовать <a href="#multiple-accounts">специальный профиль</a> для управления каналом,
|
||||
а также предварительно проверить пригодность релея.</p>
|
||||
|
||||
<p>Не стоит опасаться: архитектура Delta Chat позволяет использовать любой релей (relay-agnostic),
|
||||
поэтому вы можете легко изменить его в любой момент -
|
||||
ваши текущие подписчики этого даже не заметят.
|
||||
В этом случае достаточно будет обновить ссылку-приглашение, которую вы передаёте новым пользователям.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Звонки <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat поддерживает <strong>аудио-</strong> и <strong>видеозвонки</strong> в режиме “один-на-один”.</p>
|
||||
|
||||
<p>Звонки работают на ПК, Ubuntu Touch, iOS и Android версии 8 и новее.</p>
|
||||
|
||||
<h3 id="как-сделать-звонок">
|
||||
|
||||
|
||||
Как сделать звонок <a href="#как-сделать-звонок" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>В чате “один-на-один” нажмите на 📞 <strong>значок вызова</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Откроется небольшое меню
|
||||
в котором вы сможете выбрать вид связи <strong>аудио-</strong> или <strong>видеозвонок</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="принять-или-отклонить-вызов">
|
||||
|
||||
|
||||
Принять или отклонить вызов <a href="#принять-или-отклонить-вызов" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>При входящем звонке,
|
||||
Delta Chat показывает <strong>экран входящего вызова</strong> или уведомление.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Нажмите <strong>Принять</strong> чтобы ответить
|
||||
или <strong>Отклонить</strong> чтобы сбросить звонок.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="во-время-звонка">
|
||||
|
||||
|
||||
Во время звонка <a href="#во-время-звонка" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Вы можете <strong>отключить</strong> звук микрофона.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Вы можете <strong>включить или выключить камеру</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>На мобильных устройствах можно <strong>переключаться между фронтальной и основной камерами</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>В зависимости от устройства вы можете выбрать источник аудиовыхода или использовать режим “картинка в картинке”.
|
||||
В приложении для ПК звонок осуществляется в отдельном окне,
|
||||
что позволяет продолжать работу в основном окне Delta Chat в обычном режиме.</p>
|
||||
|
||||
<h3 id="пропущенные-вызовы-и-уведомления">
|
||||
|
||||
|
||||
Пропущенные вызовы и уведомления <a href="#пропущенные-вызовы-и-уведомления" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Если вы не ответите на звонок, не услышите сигнал или ваше устройство будет недоступно,
|
||||
вызов отобразится как <strong>пропущенный</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Только ваши подтвержденные контакты</strong> могут заставить ваше устройство звонить.
|
||||
Запросы на добавление в контакты будут приходить как обычно, но вызова не будет.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В разделе <strong>Настройки → Уведомления → Звонки</strong>,
|
||||
вы можете полностью отключить специальный экран входящего вызова.
|
||||
Если вы это сделаете, никакие уведомления о звонках не будут вас беспокоить;
|
||||
при этом вы всё равно сможете принять звонок, нажав на иконку сообщения о входящем звонке в соответствующем чате.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1418,25 +1624,20 @@ Delta Chat вместо этого использует реализацию Ope
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Используемый <a href="#relays">релей</a> должен знать ваш IP-адрес,
|
||||
а также иногда устройства ваших контактов, если вы проводите совместные <a href="#experiments">звонки</a>
|
||||
или используете <a href="#webxdc">приложения</a>.</p>
|
||||
<p>Используемым <a href="#relays">релеям</a> необходимо знать ваш IP-адрес,
|
||||
а в некоторых случаях — данные устройств ваших контактов, если вы совершаете <a href="#calls">вызов</a>
|
||||
или совместно используете <a href="#webxdc">приложения</a>.</p>
|
||||
|
||||
<p>IP-адреса необходимы для обеспечения соединения и эффективности.
|
||||
Они не сохраняются и не передаются третьим лицам.
|
||||
Обратите внимание, что IP-адрес</p>
|
||||
<ul>
|
||||
<li>это не подробный адрес, который вы указываете службе доставки,
|
||||
а скорее приблизительный, обычно определяющий регион или страну.</li>
|
||||
</ul>
|
||||
<p>IP-адреса необходимы для обеспечения связи и эффективной работы.
|
||||
Delta Chat не сохраняет их и не раскрывает третьим лицам.
|
||||
Обратите внимание, что IP-адрес
|
||||
— это не тот же адрес, который вы указываете службе доставки,
|
||||
он, как правило, менее точен и зачастую позволяет определить лишь город или регион.</p>
|
||||
|
||||
<p>Поскольку именно так по умолчанию работает интернет и другие мессенджеры,
|
||||
мы не предлагаем здесь никаких настроек и не задаём предварительных вопросов</p>
|
||||
|
||||
<p>Если вы считаете свой IP-адрес угрозой безопасности или конфиденциальности,
|
||||
мы рекомендуем использовать VPN в сочетании с режимом блокировки системы.
|
||||
Поиск настроек во всех приложениях на вашем устройстве оставит уязвимости.
|
||||
Например, нажатие на ссылку раскрывает IP-адрес неизвестным лицам и представляет собой гораздо больший риск в данном случае.</p>
|
||||
<p>Если вы считаете свой IP-адрес зоной риска,
|
||||
мы рекомендуем использовать VPN для всей системы.
|
||||
Настройка VPN для отдельных приложений оставляет уязвимости в общей защите устройства.
|
||||
Например, нажатие на ссылку может раскрыть ваш IP-адрес неизвестным сторонам, что представляет собой гораздо больший риск.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
<ul>
|
||||
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
|
||||
@@ -627,6 +642,197 @@ but more than 150 is not recommended.</p>
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1425,23 +1631,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
<ul>
|
||||
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
|
||||
@@ -627,6 +642,197 @@ but more than 150 is not recommended.</p>
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1427,23 +1633,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<li><a href="#що-таке-delta-chat">Що таке Delta Chat?</a>
|
||||
<ul>
|
||||
<li><a href="#howtoe2ee">Як мені знайти людей для спілкування?</a></li>
|
||||
<li><a href="#why-is-a-chat-marked-as-request">Why is a chat marked as “Request”?</a></li>
|
||||
<li><a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other">How can I put two of my friends in contact with each other?</a></li>
|
||||
<li><a href="#чому-чат-позначений-як-запит">Чому чат позначений як «Запит»?</a></li>
|
||||
<li><a href="#як-я-можу-познайомити-двох-своїх-друзів-один-з-одним">Як я можу познайомити двох своїх друзів один з одним?</a></li>
|
||||
<li><a href="#multiple-accounts">Що таке профілі? Як я можу перемикатися між ними?</a></li>
|
||||
<li><a href="#хто-бачить-моє-зображення-профілю">Хто бачить моє зображення профілю?</a></li>
|
||||
<li><a href="#signature">Чи можу я встановити біографію/статус у Delta Chat?</a></li>
|
||||
@@ -28,6 +28,21 @@
|
||||
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">In-chat apps</a>
|
||||
<ul>
|
||||
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
|
||||
@@ -151,8 +166,7 @@
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If both sides are online, they will soon see a chat
|
||||
and can start messaging securely.</p>
|
||||
<p>Якщо обидві сторони перебувають у мережі, незабаром з’явиться вікно чату, і вони зможуть безпечно обмінюватися повідомленнями.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If one side is offline or in bad network,
|
||||
@@ -164,19 +178,17 @@ the ability to chat is delayed until connectivity is restored.</p>
|
||||
Тепер Ви автоматично використовуватимете <a href="#e2ee">наскрізне шифрування</a> з цим контактом.
|
||||
Якщо ви додасте один одного у <a href="#groups">групи</a>, наскрізне шифрування буде встановлено між усіма учасниками.</p>
|
||||
|
||||
<h3 id="why-is-a-chat-marked-as-request">
|
||||
<h3 id="чому-чат-позначений-як-запит">
|
||||
|
||||
|
||||
Why is a chat marked as “Request”? <a href="#why-is-a-chat-marked-as-request" class="anchor"></a>
|
||||
Чому чат позначений як «Запит»? <a href="#чому-чат-позначений-як-запит" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>As being a private messenger,
|
||||
only friends and family you <a href="#howtoe2ee">share your QR code or invite link with</a> can write to you.</p>
|
||||
<p>Оскільки це приватний месенджер, лише друзі та родичі, яким ви <a href="#howtoe2ee">надіслали свій QR-код або посилання-запрошення</a>, можуть вам писати.</p>
|
||||
|
||||
<p>Your friends may share your contact with other friends,
|
||||
this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Request</b></p>
|
||||
<p>Ваші друзі можуть поділитися вашими контактними даними з іншими друзями, це відображається як <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Запит</b></p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
@@ -190,19 +202,17 @@ this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-s
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="how-can-i-put-two-of-my-friends-in-contact-with-each-other">
|
||||
<h3 id="як-я-можу-познайомити-двох-своїх-друзів-один-з-одним">
|
||||
|
||||
|
||||
How can I put two of my friends in contact with each other? <a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other" class="anchor"></a>
|
||||
Як я можу познайомити двох своїх друзів один з одним? <a href="#як-я-можу-познайомити-двох-своїх-друзів-один-з-одним" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Attach the first contact to the chat of the second using <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attachment Button → Contact</strong>.
|
||||
You can also add a little introduction message.</p>
|
||||
<p>Додайте перший контакт до чату другого, скориставшись функцією <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Кнопка «Додати» → Контакт</strong>. Ви також можете додати коротке повідомлення-представлення.</p>
|
||||
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
<p>Тоді другий контакт отримає <strong>картку</strong> і зможе натиснути на неї, щоб почати чат із першим контактом.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
@@ -212,9 +222,7 @@ and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>A profile is <strong>a name, a picture</strong> and some additional information for encrypting messages.
|
||||
A profile lives on your device(s) only
|
||||
and uses the server only to relay messages.</p>
|
||||
<p>Профіль — це <strong>ім’я, зображення</strong> та деяка додаткова інформація для шифрування повідомлень. Профіль зберігається виключно на вашому пристрої (пристроях) і використовує сервер лише для передачі повідомлень.</p>
|
||||
|
||||
<p>Під час першого встановлення Delta Chat створюється перший профіль.</p>
|
||||
|
||||
@@ -244,10 +252,7 @@ and uses the server only to relay messages.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes,
|
||||
you can do so under <strong>Settings → Profile → Bio</strong>.
|
||||
Once you sent a message to a contact,
|
||||
they will see it when they view your contact details.</p>
|
||||
<p>Так, ви можете це зробити в розділі <strong>Налаштування → Профіль → Опис</strong>. Після того як ви надішлете повідомлення контакту, він побачить його, переглянувши ваші контактні дані.</p>
|
||||
|
||||
<h3 id="що-значить-закріплення-приглушення-архівування">
|
||||
|
||||
@@ -315,10 +320,7 @@ they will see it when they view your contact details.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>You can sometimes see a <strong>green dot</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />
|
||||
next to the avatar of a contact.
|
||||
It means they were <strong>recently seen by you</strong> in the last 10 minutes,
|
||||
e.g. because they messaged you or sent a read receipt.</p>
|
||||
<p>Іноді біля аватара контакту можна побачити <strong>зелену крапку</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />. Це означає, що ви <strong>нещодавно бачили його</strong> протягом останніх 10 хвилин, наприклад, тому що він надіслав вам повідомлення або підтвердження прочитання.</p>
|
||||
|
||||
<p>So this is not a real time online status
|
||||
and others will as well not always see that you are “online”.</p>
|
||||
@@ -582,6 +584,197 @@ but more than 150 is not recommended.</p>
|
||||
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
|
||||
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbar’s number</a> for more insights.</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1268,23 +1461,20 @@ can not be identified easily.</p>
|
||||
|
||||
</h3>
|
||||
|
||||
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#experiments">call</a>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
They are neither persisted nor exposed.
|
||||
Note that the IP Address
|
||||
is not like a detailed address you give to a delivery service,
|
||||
but much more coarse, often defining region or country only.</p>
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>As this is just how the internet and other messengers work by default,
|
||||
we do not offer options here or ask upfront questions.</p>
|
||||
|
||||
<p>If you see your IP Address as a security or privacy risk,
|
||||
we recommend to use a VPN, in combination with system lockdown mode.
|
||||
Hunting down options in all apps on your system will leave gaps.
|
||||
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<h3 id="sealedsender">
|
||||
|
||||
|
||||
@@ -32,6 +32,21 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#channels">Channels</a>
|
||||
<ul>
|
||||
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
|
||||
<li><a href="#create-a-channel">Create a channel</a></li>
|
||||
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#calls">Calls</a>
|
||||
<ul>
|
||||
<li><a href="#place-a-call">Place a call</a></li>
|
||||
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
|
||||
<li><a href="#during-a-call">During a call</a></li>
|
||||
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#webxdc">Webxdc 应用</a>
|
||||
<ul>
|
||||
<li><a href="#我在哪里可以获得-webxdc-应用">我在哪里可以获得 Webxdc 应用?</a></li>
|
||||
@@ -621,6 +636,197 @@ Please use this method sparingly, as sending original files will significantly i
|
||||
其中Delta Chat 是与<a href="#groups">平等权利</a> 聊天的私人信使。
|
||||
相关知识,请参阅<a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">邓巴数</a>。</p>
|
||||
|
||||
<h2 id="channels">
|
||||
|
||||
|
||||
Channels <a href="#channels" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Channels are a one-to-many tool for broadcasting messages.</p>
|
||||
|
||||
<h3 id="subscribe-to-a-channel">
|
||||
|
||||
|
||||
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
|
||||
or tap the <strong>invite link</strong> you got from the channel owner.</li>
|
||||
</ul>
|
||||
|
||||
<p>That’s all!
|
||||
You will receive a few of the messages from the channel history
|
||||
and, from that point on, all new messages from the channel.</p>
|
||||
|
||||
<p><strong>Don’t worry,</strong> if that does not happen immediately.
|
||||
Once the channel owner comes online, your join request will be processed.</p>
|
||||
|
||||
<p>As all of Delta Chat, also Channels are private and decentralized,
|
||||
there is no public discovery.</p>
|
||||
|
||||
<p>Other channel subscribers will not see that you subscribed and cannot message you.
|
||||
The channel owner, however, can message you.
|
||||
They will also see that you read a message unless you have read receipts disabled.</p>
|
||||
|
||||
<p>If you do not want to share your main profile,
|
||||
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
|
||||
|
||||
<h3 id="create-a-channel">
|
||||
|
||||
|
||||
Create a channel <a href="#create-a-channel" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can now send and manage messages as usual.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>From the channel’s profile, <strong>share the QR code or invite link with others</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Subscribers will receive your messages,
|
||||
but they cannot send messages in your channel.
|
||||
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
|
||||
|
||||
<p>You can see the <strong>view count</strong> beside each message.
|
||||
Note that this only counts subscribers who have read receipts enabled,
|
||||
so the real view count may be larger.</p>
|
||||
|
||||
<h3 id="how-many-subscribers-can-a-channel-have">
|
||||
|
||||
|
||||
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
|
||||
|
||||
<p>The practical limit depends on the used <a href="#relays">relay</a>,
|
||||
so there is no single fixed number that applies everywhere.</p>
|
||||
|
||||
<p>For really large channels with several tens of thousands of subscribers,
|
||||
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
|
||||
and checking whether the relay is suitable.</p>
|
||||
|
||||
<p>But don’t be too hesitant: Delta Chat is designed to be relay-agnostic,
|
||||
so you can change your relay at any point easily -
|
||||
your existing subscribers will not even notice.
|
||||
You only have to update the invite link you share with new subscribers in that case.</p>
|
||||
|
||||
<h2 id="calls">
|
||||
|
||||
|
||||
Calls <a href="#calls" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
|
||||
|
||||
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
|
||||
|
||||
<h3 id="place-a-call">
|
||||
|
||||
|
||||
Place a call <a href="#place-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>This opens a small menu
|
||||
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="accept-or-reject-a-call">
|
||||
|
||||
|
||||
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>When someone calls you,
|
||||
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tap <strong>Accept</strong> to answer
|
||||
or <strong>Decline</strong> to reject the call.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="during-a-call">
|
||||
|
||||
|
||||
During a call <a href="#during-a-call" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>mute</strong> your microphone.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>You can <strong>enable or disable your camera</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
|
||||
On desktop, the call is using a dedicated window
|
||||
and you can continue using the main Delta Chat window as usual.</p>
|
||||
|
||||
<h3 id="missed-calls-and-notifications">
|
||||
|
||||
|
||||
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
|
||||
the call appears as a <strong>missed call</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Only your accepted contacts</strong> can make your device ring.
|
||||
Contact requests will appear as usual and will not ring.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>At <strong>Settings → Notifications → Calls</strong>,
|
||||
you can disable the special call ringing screen completely.
|
||||
If you do so, you will not be disturbed by any ringing notification,
|
||||
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="webxdc">
|
||||
|
||||
|
||||
@@ -1414,22 +1620,20 @@ Delta Chat 应用程序不会在服务器上存储任何有关联系人或群组
|
||||
|
||||
</h4>
|
||||
|
||||
<p>使用的 <a href="#relays">中继服务器</a> 需要知道您的 IP 地址、
|
||||
有时还需要知道联系人的设备(如果你们有 <a href="#experiments">通话</a>),或一起使用 <a href="#webxdc">Webxdc应用程序</a>。</p>
|
||||
<p>The used <a href="#relays">relays</a> need to know your IP Address,
|
||||
as well as sometimes your contact’s devices if you have a <a href="#calls">call</a>
|
||||
or use <a href="#webxdc">apps</a> together.</p>
|
||||
|
||||
<p>IP 地址是连接和提高效率所必需的。
|
||||
它们既不会持久存在,也不会暴露。
|
||||
请注意,IP 地址
|
||||
不像你给快递服务的详细地址、
|
||||
而是更粗略,通常只定义地区或国家。</p>
|
||||
<p>IP Addresses are needed for connectivity and efficiency.
|
||||
Delta Chat neither persists nor exposes them.
|
||||
Note that IP Addresses
|
||||
are not like an address you give to a delivery service,
|
||||
but typically less precise, often defining city or region only.</p>
|
||||
|
||||
<p>这只是互联网和其他信使的默认工作方式、
|
||||
我们在此不提供选项,也不预先提问。</p>
|
||||
|
||||
<p>如果你认为你的 IP 地址存在安全或隐私风险、
|
||||
我们建议使用 VPN 并结合系统锁定模式。
|
||||
在系统的所有应用程序中查找选项会留下漏洞。
|
||||
例如,点击链接会将 IP 地址暴露给未知方,这是目前最大的风险。</p>
|
||||
<p>If you see your IP Address as a risk,
|
||||
we recommend to use a VPN for the whole system.
|
||||
Per-app options leave gaps across your system.
|
||||
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
|
||||
|
||||
<p>###Delta Chat 是否支持 “密封发件人”?{#sealedsender}</p>
|
||||
|
||||
|
||||
@@ -443,10 +443,19 @@ public class Rpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
* by the set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
* Estimates the number of messages that will be deleted
|
||||
* by the `set_config()`-option `delete_device_after`.
|
||||
* <p>
|
||||
* This is typically used to show the estimated impact to the user
|
||||
* before actually enabling deletion of old messages.
|
||||
* <p>
|
||||
* Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
|
||||
* <p>
|
||||
* Parameters:
|
||||
* - `from_server`: Deprecated, pass `false` here
|
||||
* - `seconds`: Count messages older than the given number of seconds.
|
||||
* <p>
|
||||
* Returns the number of messages that are older than the given number of seconds.
|
||||
*/
|
||||
public Integer estimateAutoDeletionCount(Integer accountId, Boolean fromServer, Integer seconds) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "estimate_auto_deletion_count", mapper.valueToTree(accountId), mapper.valueToTree(fromServer), mapper.valueToTree(seconds));
|
||||
@@ -710,9 +719,6 @@ public class Rpc {
|
||||
* because the word "channel" already appears a lot in the code,
|
||||
* which would make it hard to grep for it.
|
||||
* <p>
|
||||
* After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||
* see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
|
||||
* <p>
|
||||
* Returns the created chat's id.
|
||||
*/
|
||||
public Integer createBroadcast(Integer accountId, String chatName) throws RpcException {
|
||||
@@ -913,8 +919,22 @@ public class Rpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all messages of a particular chat.
|
||||
* Get all message IDs belonging to a chat.
|
||||
* <p>
|
||||
* The list is already sorted and starts with the oldest message.
|
||||
* Clients should not try to re-sort the list as this would be an expensive action
|
||||
* and would result in inconsistencies between clients.
|
||||
* Note that the messages are not necessarily sorted by their ID or by their displayed timestamp;
|
||||
* UIs need to handle both the case of descending message IDs
|
||||
* and of decreasing timestamps.
|
||||
* <p>
|
||||
* Optionally, 'daymarkers' added to the ID array may help to
|
||||
* implement virtual lists.
|
||||
* <p>
|
||||
* Parameters:
|
||||
* <p>
|
||||
* * chat_id The chat ID of which the messages IDs should be queried.
|
||||
* * _info_only: Deprecated, pass `false` here.
|
||||
* * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
|
||||
* e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
||||
* corresponding (following) day in the local timezone.
|
||||
@@ -932,6 +952,14 @@ public class Rpc {
|
||||
return transport.callForResult(new TypeReference<java.util.List<Integer>>(){}, "get_existing_msg_ids", mapper.valueToTree(accountId), mapper.valueToTree(msgIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages belonging to a chat.
|
||||
* <p>
|
||||
* Similar to `get_message_ids` / `getMessageIds`,
|
||||
* see that function for details.
|
||||
* The difference is that this function here returns a list of `MessageListItem`,
|
||||
* which is an enum of a message or a daymarker.
|
||||
*/
|
||||
public java.util.List<MessageListItem> getMessageListItems(Integer accountId, Integer chatId, Boolean infoOnly, Boolean addDaymarker) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<java.util.List<MessageListItem>>(){}, "get_message_list_items", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(infoOnly), mapper.valueToTree(addDaymarker));
|
||||
}
|
||||
@@ -1181,11 +1209,6 @@ public class Rpc {
|
||||
return transport.callForResult(new TypeReference<String>(){}, "make_vcard", mapper.valueToTree(accountId), mapper.valueToTree(contacts));
|
||||
}
|
||||
|
||||
/** Sets vCard containing the given contacts to the message draft. */
|
||||
public void setDraftVcard(Integer accountId, Integer msgId, java.util.List<Integer> contacts) throws RpcException {
|
||||
transport.call("set_draft_vcard", mapper.valueToTree(accountId), mapper.valueToTree(msgId), mapper.valueToTree(contacts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||
* <p>
|
||||
@@ -1328,10 +1351,47 @@ public class Rpc {
|
||||
return transport.callForResult(new TypeReference<String>(){}, "get_connectivity_html", mapper.valueToTree(accountId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current location.
|
||||
* <p>
|
||||
* Returns true if location streaming is currently
|
||||
* enabled and locations should be updated.
|
||||
* <p>
|
||||
* Location is represented as latitude and longitude in degrees
|
||||
* and horizontal accuracy in meters.
|
||||
*/
|
||||
public Boolean setLocation(Float latitude, Float longitude, Float accuracy) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Boolean>(){}, "set_location", mapper.valueToTree(latitude), mapper.valueToTree(longitude), mapper.valueToTree(accuracy));
|
||||
}
|
||||
|
||||
public java.util.List<Location> getLocations(Integer accountId, Integer chatId, Integer contactId, Integer timestampBegin, Integer timestampEnd) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<java.util.List<Location>>(){}, "get_locations", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(contactId), mapper.valueToTree(timestampBegin), mapper.valueToTree(timestampEnd));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
|
||||
* <p>
|
||||
* Pass 0 as the number of seconds to disable location streaming in the chat.
|
||||
*/
|
||||
public void sendLocationsToChat(Integer accountId, Integer chatId, Integer seconds) throws RpcException {
|
||||
transport.call("send_locations_to_chat", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(seconds));
|
||||
}
|
||||
|
||||
/** Returns whether any chat is sending locations. */
|
||||
public Boolean isSendingLocations(Integer accountId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Boolean>(){}, "is_sending_locations", mapper.valueToTree(accountId));
|
||||
}
|
||||
|
||||
/** Returns whether `chat_id` is sending locations. */
|
||||
public Boolean isSendingLocationsToChat(Integer accountId, Integer chatId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Boolean>(){}, "is_sending_locations_to_chat", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
|
||||
}
|
||||
|
||||
/** Stops sending locations to all chats. */
|
||||
public void stopSendingLocations() throws RpcException {
|
||||
transport.call("stop_sending_locations");
|
||||
}
|
||||
|
||||
public void sendWebxdcStatusUpdate(Integer accountId, Integer instanceMsgId, String updateStr, String descr) throws RpcException {
|
||||
transport.call("send_webxdc_status_update", mapper.valueToTree(accountId), mapper.valueToTree(instanceMsgId), mapper.valueToTree(updateStr), mapper.valueToTree(descr));
|
||||
}
|
||||
@@ -1467,17 +1527,18 @@ public class Rpc {
|
||||
transport.call("resend_messages", mapper.valueToTree(accountId), mapper.valueToTree(messageIds));
|
||||
}
|
||||
|
||||
/** @deprecated as of 2026-04; use `send_msg` with `Viewtype::Sticker` instead. */
|
||||
public Integer sendSticker(Integer accountId, Integer chatId, String stickerPath) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "send_sticker", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(stickerPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reaction to message.
|
||||
* Sends a reaction to message.
|
||||
* <p>
|
||||
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||
* single message can be sent multiple times. The last reaction
|
||||
* received overrides all previously received reactions. It is
|
||||
* possible to remove all reactions by sending an empty string.
|
||||
* A reaction is a string that represents an emoji.
|
||||
* You can call this function again to change the emoji;
|
||||
* the last sent reaction overrides all previously sent reactions.
|
||||
* It is possible to remove the reaction by sending an empty string.
|
||||
*/
|
||||
public Integer sendReaction(Integer accountId, Integer messageId, java.util.List<String> reaction) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "send_reaction", mapper.valueToTree(accountId), mapper.valueToTree(messageId), mapper.valueToTree(reaction));
|
||||
|
||||
@@ -12,6 +12,13 @@ public class EnteredLoginParam {
|
||||
/** TLS options: whether to allow invalid certificates and/or invalid hostnames. Default: Automatic */
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public EnteredCertificateChecks certificateChecks;
|
||||
/**
|
||||
* IMAP server folder.
|
||||
* <p>
|
||||
* Defaults to "INBOX" if not set. Should not be an empty string.
|
||||
*/
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public String imapFolder;
|
||||
/** Imap server port. */
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public Integer imapPort;
|
||||
|
||||
@@ -5,6 +5,6 @@ package chat.delta.rpc.types;
|
||||
public class Reactions {
|
||||
/** Unique reactions and their count, sorted in descending order. */
|
||||
public java.util.List<Reaction> reactions;
|
||||
/** Map from a contact to it's reaction to message. */
|
||||
/** Map from a contact to it's reaction to message. There is only a single reaction per contact, but this contains a list of reactions for historical reasons. */
|
||||
public java.util.Map<String, java.util.List<String>> reactionsByContact;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public enum Viewtype {
|
||||
Gif,
|
||||
|
||||
/**
|
||||
* Message containing a sticker, similar to image. NB: When sending, the message viewtype may be changed to `Image` by some heuristics like checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
||||
* Message containing a sticker, similar to image.
|
||||
* <p>
|
||||
* If possible, the ui should display the image without borders in a transparent way. A click on a sticker will offer to install the sticker set in some future.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
package chat.delta.rpc.types;
|
||||
|
||||
public class WebxdcMessageInfo {
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public String orientation;
|
||||
/** if the Webxdc represents a document, then this is the name of the document */
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public String document;
|
||||
@@ -15,6 +17,10 @@ public class WebxdcMessageInfo {
|
||||
public String icon;
|
||||
/** True if full internet access should be granted to the app. */
|
||||
public Boolean internetAccess;
|
||||
/** Define if the local user is the one who initially shared the webxdc application in the chat. */
|
||||
public Boolean isAppSender;
|
||||
/** Define if the app runs in a broadcasting context. */
|
||||
public Boolean isBroadcast;
|
||||
/**
|
||||
* The name of the app.
|
||||
* <p>
|
||||
|
||||
@@ -84,7 +84,7 @@ public class DcAccounts {
|
||||
public boolean isAllChatmail() {
|
||||
for (int accountId : getAll()) {
|
||||
DcContext dcContext = getAccount(accountId);
|
||||
if (!dcContext.isChatmail()) {
|
||||
if (dcContext.getConfigInt("is_chatmail") == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,10 +362,6 @@ public class DcContext {
|
||||
return displayname;
|
||||
}
|
||||
|
||||
public boolean isChatmail() {
|
||||
return getConfigInt("is_chatmail") == 1;
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return getConfigInt("is_muted") == 1;
|
||||
}
|
||||
|
||||
@@ -194,8 +194,6 @@ public class DcMsg {
|
||||
|
||||
public native void setHtml(String text);
|
||||
|
||||
public native void forceSticker();
|
||||
|
||||
public native void setFileAndDeduplicate(String file, String name, String filemime);
|
||||
|
||||
public native void setDimension(int width, int height);
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
@@ -12,18 +11,19 @@ import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import java.util.ArrayList;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
@@ -55,7 +55,6 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
this.type3 = type3;
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
private DcContext dcContext;
|
||||
private int chatId;
|
||||
@@ -64,7 +63,7 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
private final ArrayList<TabData> tabs = new ArrayList<>();
|
||||
private Toolbar toolbar;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
private @Nullable MediaController mediaController;
|
||||
private ListenableFuture<MediaController> mediaControllerFuture;
|
||||
@@ -98,8 +97,20 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
isGlobalGallery() ? R.string.menu_all_media : R.string.apps_and_media);
|
||||
}
|
||||
|
||||
this.tabLayout.setupWithViewPager(viewPager);
|
||||
this.viewPager.setAdapter(new AllMediaPagerAdapter(getSupportFragmentManager()));
|
||||
AllMediaPagerAdapter adapter = new AllMediaPagerAdapter(this);
|
||||
this.viewPager.setAdapter(adapter);
|
||||
this.viewPager.registerOnPageChangeCallback(
|
||||
new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
adapter.onPageChanged(position);
|
||||
}
|
||||
});
|
||||
new TabLayoutMediator(
|
||||
this.tabLayout,
|
||||
this.viewPager,
|
||||
(tab, position) -> tab.setText(getString(tabs.get(position).title)))
|
||||
.attach();
|
||||
if (getIntent().getBooleanExtra(FORCE_GALLERY, false)) {
|
||||
this.viewPager.setCurrentItem(1, false);
|
||||
}
|
||||
@@ -186,31 +197,16 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
return contactId == 0 && chatId == 0;
|
||||
}
|
||||
|
||||
private class AllMediaPagerAdapter extends FragmentStatePagerAdapter {
|
||||
private Object currentFragment = null;
|
||||
private class AllMediaPagerAdapter extends FragmentStateAdapter {
|
||||
private int currentPosition = -1;
|
||||
|
||||
AllMediaPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
super.setPrimaryItem(container, position, object);
|
||||
if (currentFragment != null && currentFragment != object) {
|
||||
ActionMode action = null;
|
||||
if (currentFragment instanceof MessageSelectorFragment) {
|
||||
action = ((MessageSelectorFragment) currentFragment).getActionMode();
|
||||
}
|
||||
if (action != null) {
|
||||
action.finish();
|
||||
}
|
||||
}
|
||||
currentFragment = object;
|
||||
AllMediaPagerAdapter(FragmentActivity activity) {
|
||||
super(activity);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
public Fragment createFragment(int position) {
|
||||
TabData data = tabs.get(position);
|
||||
Fragment fragment;
|
||||
Bundle args = new Bundle();
|
||||
@@ -233,13 +229,24 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
return tabs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return getString(tabs.get(position).title);
|
||||
private void onPageChanged(int newPosition) {
|
||||
if (currentPosition != -1 && currentPosition != newPosition) {
|
||||
for (Fragment fragment : getSupportFragmentManager().getFragments()) {
|
||||
if (!(fragment instanceof MessageSelectorFragment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ActionMode action = ((MessageSelectorFragment) fragment).getActionMode();
|
||||
if (action != null) {
|
||||
action.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
currentPosition = newPosition;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -255,12 +255,6 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
// 2025-12-16: The setting was removed.
|
||||
// Revert it to the default if it was changed in the past.
|
||||
ac.setConfigInt("webxdc_realtime_enabled", 1);
|
||||
|
||||
// 2025-11-12: this is needed until core starts ignoring "delete_server_after" for
|
||||
// chatmail
|
||||
if (ac.isChatmail()) {
|
||||
ac.setConfig("delete_server_after", null); // reset
|
||||
}
|
||||
}
|
||||
if (allAccounts.length == 0) {
|
||||
try {
|
||||
@@ -308,7 +302,10 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
Log.i(
|
||||
"DeltaChat",
|
||||
"++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
|
||||
getDcAccounts().maybeNetwork();
|
||||
// onBlockedStatusChanged is only available on API 29+
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
getDcAccounts().maybeNetwork();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -316,8 +313,13 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
@NonNull android.net.Network network, boolean blocked) {
|
||||
Log.i(
|
||||
"DeltaChat",
|
||||
"++++++++++++++++++ NetworkCallback.onBlockedStatusChanged() #"
|
||||
"++++++++++++++++++ NetworkCallback.onBlockedStatusChanged("
|
||||
+ blocked
|
||||
+ ") #"
|
||||
+ debugOnBlockedStatusChangedCount++);
|
||||
if (!blocked) {
|
||||
getDcAccounts().maybeNetwork();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.connect.DirectShareUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||
@@ -423,9 +422,18 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
.setIcon(IconCompat.createWithAdaptiveBitmap(avatar))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
|
||||
boolean success;
|
||||
try {
|
||||
success = ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfoCompat, null);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "ErrAddToHomescreen: requestPinShortcut() failed", e);
|
||||
success = false;
|
||||
}
|
||||
boolean finalSuccess = success;
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
if (!ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfoCompat, null)) {
|
||||
if (!finalSuccess) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
"ErrAddToHomescreen: requestPinShortcut() failed",
|
||||
@@ -479,10 +487,6 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
if (isRelayingMessageContent(getActivity())) {
|
||||
if (ShareUtil.getSharedContactId(getActivity()) != 0) {
|
||||
return false; // no sharing of a contact to multiple recipients at the same time, we can
|
||||
// reconsider when that becomes a real-world need
|
||||
}
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
fab.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_send_sms_white_24dp));
|
||||
|
||||
@@ -168,6 +168,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private static final int RECORD_VIDEO = 8;
|
||||
private static final int PICK_WEBXDC = 9;
|
||||
|
||||
private static final Object searchLock = new Object();
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
protected ComposeText composeText;
|
||||
private AnimatingToggle buttonToggle;
|
||||
@@ -176,7 +178,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
protected ConversationTitleView titleView;
|
||||
private ConversationFragment fragment;
|
||||
private InputAwareLayout container;
|
||||
private View composePanel;
|
||||
private ScaleStableImageView backgroundView;
|
||||
private MessageRequestsBottomView messageRequestBottomView;
|
||||
private ProgressDialog progressDialog;
|
||||
@@ -200,7 +201,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private final boolean isSecureText = true;
|
||||
private boolean isDefaultSms = true;
|
||||
private boolean isSecurityInitialized = false;
|
||||
private boolean successfulForwardingAttempt = false;
|
||||
private boolean isEditing = false;
|
||||
private boolean switchedProfile = false;
|
||||
|
||||
@@ -264,6 +264,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
public void handleOnBackPressed() {
|
||||
if (container.isInputOpen()) {
|
||||
container.hideCurrentInput(composeText);
|
||||
} else if (searchMenu != null) {
|
||||
searchCollapse();
|
||||
} else {
|
||||
handleReturnToConversationList();
|
||||
}
|
||||
@@ -280,6 +282,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
eventCenter.removeObservers(this);
|
||||
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_DELETED, this);
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED, this);
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
|
||||
|
||||
@@ -387,7 +390,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
processComposeControls(ACTION_SAVE_DRAFT);
|
||||
if (inputPanel.isRecording() && inputPanel.getRecordingDuration() > 1000) {
|
||||
saveRecording();
|
||||
} else {
|
||||
processComposeControls(ACTION_SAVE_DRAFT);
|
||||
}
|
||||
|
||||
DcHelper.getNotificationCenter(this).clearVisibleChat();
|
||||
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
|
||||
@@ -440,32 +447,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
else mediaType = MediaType.IMAGE;
|
||||
setMedia(singleUri, mediaType);
|
||||
} else {
|
||||
final ClipData multipleUris = data.getClipData();
|
||||
if (multipleUris != null) {
|
||||
final int uriCount = multipleUris.getItemCount();
|
||||
if (uriCount > 0) {
|
||||
ArrayList<Uri> uriList = new ArrayList<>(uriCount);
|
||||
for (int i = 0; i < uriCount; i++) {
|
||||
uriList.add(multipleUris.getItemAt(i).getUri());
|
||||
}
|
||||
askSendingFiles(
|
||||
uriList,
|
||||
() -> {
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
SendRelayedMessageUtil.sendMultipleMsgs(this, chatId, uriList, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
sendMultipleMsgs(data);
|
||||
}
|
||||
break;
|
||||
|
||||
case PICK_DOCUMENT:
|
||||
final String docMimeType = MediaUtil.getMimeType(this, data.getData());
|
||||
final MediaType docMediaType =
|
||||
MediaUtil.isAudioType(docMimeType) ? MediaType.AUDIO : MediaType.DOCUMENT;
|
||||
setMedia(data.getData(), docMediaType);
|
||||
if (data.getData() != null) { // single Uri
|
||||
final String docMimeType = MediaUtil.getMimeType(this, data.getData());
|
||||
final MediaType docMediaType =
|
||||
MediaUtil.isAudioType(docMimeType) ? MediaType.AUDIO : MediaType.DOCUMENT;
|
||||
setMedia(data.getData(), docMediaType);
|
||||
} else {
|
||||
sendMultipleMsgs(data);
|
||||
}
|
||||
break;
|
||||
|
||||
case PICK_WEBXDC:
|
||||
@@ -508,6 +502,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMultipleMsgs(Intent data) {
|
||||
final ClipData multipleUris = data.getClipData();
|
||||
if (multipleUris != null) {
|
||||
final int uriCount = multipleUris.getItemCount();
|
||||
if (uriCount > 0) {
|
||||
ArrayList<Uri> uriList = new ArrayList<>(uriCount);
|
||||
for (int i = 0; i < uriCount; i++) {
|
||||
uriList.add(multipleUris.getItemAt(i).getUri());
|
||||
}
|
||||
askSendingFiles(
|
||||
uriList,
|
||||
() -> {
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> SendRelayedMessageUtil.sendMultipleMsgs(this, chatId, uriList, null));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent) {
|
||||
if (intent.getStringExtra(Browser.EXTRA_APPLICATION_ID) != null) {
|
||||
@@ -636,12 +649,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_start_audio_call) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CallUtil.startAudioCall(context, chatId);
|
||||
CallUtil.startAudioCall(this, chatId);
|
||||
}
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_start_video_call) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CallUtil.startVideoCall(context, chatId);
|
||||
CallUtil.startVideoCall(this, chatId);
|
||||
}
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_all_media) {
|
||||
@@ -713,7 +726,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
|
||||
}
|
||||
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
if (attachmentManager.isAttachmentPresent()) {
|
||||
SlideDeck slideDeck = attachmentManager.buildSlideDeck();
|
||||
int audioDraftId = slideDeck.getAudioDraftId();
|
||||
if (audioDraftId != 0) {
|
||||
playbackViewModel.stop(audioDraftId);
|
||||
}
|
||||
}
|
||||
|
||||
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
|
||||
Intent intent =
|
||||
@@ -846,20 +865,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (dcChat.isSelfTalk()) {
|
||||
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
|
||||
} else {
|
||||
String name = dcChat.getName();
|
||||
if (!dcChat.isMultiUser()) {
|
||||
int[] contactIds = dcContext.getChatContacts(chatId);
|
||||
if (contactIds.length == 1 || contactIds.length == 2) {
|
||||
name = dcContext.getContact(contactIds[0]).getDisplayName();
|
||||
}
|
||||
}
|
||||
int messageIds[] = ShareUtil.getForwardedMessageIDs(this);
|
||||
int messageCount = messageIds == null ? 0 : messageIds.length;
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.ask_forward, name))
|
||||
.setMessage(
|
||||
getResources()
|
||||
.getQuantityString(
|
||||
R.plurals.ask_forward_messages, messageCount, messageCount, dcChat.getName()))
|
||||
.setPositiveButton(
|
||||
R.string.ok,
|
||||
R.string.forward,
|
||||
(dialogInterface, i) -> {
|
||||
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
|
||||
successfulForwardingAttempt = true;
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (dialogInterface, i) -> finish())
|
||||
.setOnCancelListener(dialog -> finish())
|
||||
@@ -883,13 +899,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private void handleSharing() {
|
||||
ArrayList<Uri> uriList = ShareUtil.getSharedUris(this);
|
||||
int sharedContactId = ShareUtil.getSharedContactId(this);
|
||||
if (uriList.size() > 1) {
|
||||
askSendingFiles(uriList, () -> SendRelayedMessageUtil.immediatelyRelay(this, chatId));
|
||||
} else {
|
||||
if (sharedContactId != 0) {
|
||||
addAttachmentContactInfo(sharedContactId);
|
||||
} else if (ShareUtil.getSharedHtml(this) != null
|
||||
if (ShareUtil.getSharedHtml(this) != null
|
||||
|| ShareUtil.getSharedSubject(this) != null
|
||||
|| ("sticker".equals(ShareUtil.getSharedType(this)) && !uriList.isEmpty())) {
|
||||
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
|
||||
@@ -977,6 +990,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
setMedia(draft, MediaType.GIF).addListener(listener);
|
||||
break;
|
||||
case DcMsg.DC_MSG_AUDIO:
|
||||
case DcMsg.DC_MSG_VOICE:
|
||||
setMedia(draft, MediaType.AUDIO).addListener(listener);
|
||||
break;
|
||||
case DcMsg.DC_MSG_VIDEO:
|
||||
@@ -1017,7 +1031,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
attachButton = ViewUtil.findById(this, R.id.attach_button);
|
||||
composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
|
||||
emojiPickerContainer = ViewUtil.findById(this, R.id.emoji_picker_container);
|
||||
composePanel = ViewUtil.findById(this, R.id.bottom_panel);
|
||||
container = ViewUtil.findById(this, R.id.layout_container);
|
||||
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
|
||||
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
|
||||
@@ -1043,7 +1056,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
// apply padding top to avoid drawing behind top bar
|
||||
ViewUtil.applyWindowInsets(findViewById(R.id.fragment_content), false, true, false, false);
|
||||
// apply padding to root to avoid collision with system bars
|
||||
ViewUtil.applyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
|
||||
ViewUtil.applyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
|
||||
ViewUtil.applyWindowInsets(emojiPickerContainer, false, false, false, true);
|
||||
|
||||
container.addOnKeyboardShownListener(this);
|
||||
container.addOnKeyboardHiddenListener(backgroundView);
|
||||
@@ -1133,22 +1147,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
recipient = new Recipient(this, dcChat);
|
||||
glideRequests = GlideApp.with(this);
|
||||
|
||||
setComposePanelVisibility(true);
|
||||
setInputPanelVisibility(true);
|
||||
initializeContactRequest();
|
||||
}
|
||||
|
||||
private void setComposePanelVisibility(boolean isInitialization) {
|
||||
private void setInputPanelVisibility(boolean isInitialization) {
|
||||
int inputPanelVisibility;
|
||||
boolean isAttachmentHidden;
|
||||
if (dcChat.canSend()) {
|
||||
composePanel.setVisibility(View.VISIBLE);
|
||||
attachmentManager.setHidden(false);
|
||||
inputPanelVisibility = View.VISIBLE;
|
||||
isAttachmentHidden = false;
|
||||
inputPanel.setSubjectVisible(!dcChat.isEncrypted());
|
||||
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input
|
||||
// bar
|
||||
// ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
|
||||
// fragment.handleRemoveBottomInsets();
|
||||
} else {
|
||||
composePanel.setVisibility(View.GONE);
|
||||
attachmentManager.setHidden(true);
|
||||
inputPanelVisibility = View.GONE;
|
||||
isAttachmentHidden = true;
|
||||
hideSoftKeyboard();
|
||||
inputPanel.setSubjectVisible(false);
|
||||
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input
|
||||
@@ -1160,6 +1176,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
*/
|
||||
}
|
||||
synchronized (searchLock) {
|
||||
if (searchMenu != null) { // in search mode, don't change visibility directly
|
||||
beforeSearchInputPanelVisibility = inputPanelVisibility;
|
||||
beforeSearchAttachmentEditorHidden = isAttachmentHidden;
|
||||
} else {
|
||||
inputPanel.setVisibility(inputPanelVisibility);
|
||||
// attachmentManager.setHidden(isAttachmentHidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////// Helper Methods
|
||||
@@ -1272,9 +1297,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
inputPanel.clearSubject();
|
||||
}
|
||||
|
||||
// Stop draft audio playback regardless, since it is unlikely
|
||||
// we will need background playback for drafts
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
// Stop draft audio playback
|
||||
if (slideDeck != null) {
|
||||
int audioDraftId = slideDeck.getAudioDraftId();
|
||||
if (audioDraftId != 0) {
|
||||
playbackViewModel.stop(audioDraftId);
|
||||
}
|
||||
}
|
||||
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
final int currentChatId = dcChat.getId();
|
||||
@@ -1624,7 +1653,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
msg.setQuote(quote.get().getQuotedMsg());
|
||||
}
|
||||
msg.setFileAndDeduplicate(path, null, null);
|
||||
msg.forceSticker();
|
||||
dcContext.sendMsg(chatId, msg);
|
||||
}
|
||||
|
||||
@@ -1686,6 +1714,56 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void saveRecording() {
|
||||
inputPanel.resetRecordingUI();
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
final int thisChatId = chatId;
|
||||
final Optional<QuoteModel> quote = inputPanel.getQuote();
|
||||
|
||||
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
|
||||
future.addListener(
|
||||
new ListenableFuture.Listener<Pair<Uri, Long>>() {
|
||||
@Override
|
||||
public void onSuccess(final @NonNull Pair<Uri, Long> result) {
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
try {
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
String path =
|
||||
DcHelper.copyToBlobdir(
|
||||
ConversationActivity.this, result.first, "voice", ".m4a");
|
||||
|
||||
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_VOICE);
|
||||
msg.setFileAndDeduplicate(path, null, null);
|
||||
if (quote.isPresent()) {
|
||||
msg.setQuote(quote.get().getQuotedMsg());
|
||||
}
|
||||
dcContext.setDraft(thisChatId, msg);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to save voice as draft", e);
|
||||
} finally {
|
||||
PersistentBlobProvider.getInstance()
|
||||
.delete(ConversationActivity.this, result.first);
|
||||
}
|
||||
|
||||
runOnUiThread(
|
||||
() -> {
|
||||
if (chatId == thisChatId && !isFinishing() && !isDestroyed()) {
|
||||
initializeDraft();
|
||||
updateToggleButtonState();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, "Failed to stop recording", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class AttachButtonListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -1800,19 +1878,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
dcChat = dcContext.getChat(chatId);
|
||||
titleView.setTitle(glideRequests, dcChat);
|
||||
initializeSecurity(isSecureText, isDefaultSms);
|
||||
setComposePanelVisibility(false);
|
||||
setInputPanelVisibility(false);
|
||||
initializeContactRequest();
|
||||
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|
||||
|| eventId == DcContext.DC_EVENT_MSG_READ)
|
||||
&& event.getData1Int() == chatId) {
|
||||
dcChat = dcContext.getChat(chatId);
|
||||
titleView.setTitle(glideRequests, dcChat);
|
||||
} else if (eventId == DcContext.DC_EVENT_CHAT_DELETED && event.getData1Int() == chatId) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
// in-chat search
|
||||
|
||||
private int beforeSearchComposeVisibility = View.VISIBLE;
|
||||
private boolean beforeSearchAttachmentEditorHidden;
|
||||
private int beforeSearchMsgRequestVisibility;
|
||||
private int beforeSearchInputPanelVisibility;
|
||||
|
||||
private Menu searchMenu = null;
|
||||
private int[] searchResult = {};
|
||||
@@ -1833,17 +1915,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void searchExpand(final Menu menu, final MenuItem searchItem) {
|
||||
searchMenu = menu;
|
||||
synchronized (searchLock) {
|
||||
searchMenu = menu;
|
||||
|
||||
beforeSearchComposeVisibility = composePanel.getVisibility();
|
||||
composePanel.setVisibility(View.GONE);
|
||||
beforeSearchAttachmentEditorHidden = attachmentManager.isHidden();
|
||||
beforeSearchMsgRequestVisibility = messageRequestBottomView.getVisibility();
|
||||
beforeSearchInputPanelVisibility = inputPanel.getVisibility();
|
||||
|
||||
// attachmentManager.setHidden(true);
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
ConversationActivity.this.makeSearchMenuVisible(menu, searchItem);
|
||||
}
|
||||
|
||||
private void searchCollapse() {
|
||||
searchMenu = null;
|
||||
composePanel.setVisibility(beforeSearchComposeVisibility);
|
||||
synchronized (searchLock) {
|
||||
searchMenu = null;
|
||||
// attachmentManager.setHidden(beforeSearchAttachmentEditorHidden);
|
||||
messageRequestBottomView.setVisibility(beforeSearchMsgRequestVisibility);
|
||||
inputPanel.setVisibility(beforeSearchInputPanelVisibility);
|
||||
}
|
||||
|
||||
// trigger onPrepareOptionsMenu() to restore correct menu visibility
|
||||
invalidateOptionsMenu();
|
||||
@@ -1904,16 +1997,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
public void initializeContactRequest() {
|
||||
if (!dcChat.isContactRequest()) {
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
synchronized (searchLock) {
|
||||
if (searchMenu != null) { // in search mode, don't change visibility directly
|
||||
beforeSearchMsgRequestVisibility = View.GONE;
|
||||
} else {
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
messageRequestBottomView.setVisibility(View.VISIBLE);
|
||||
synchronized (searchLock) {
|
||||
if (searchMenu != null) { // in search mode, don't change visibility directly
|
||||
beforeSearchMsgRequestVisibility = View.VISIBLE;
|
||||
} else {
|
||||
messageRequestBottomView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
messageRequestBottomView.setAcceptOnClickListener(
|
||||
v -> {
|
||||
DcHelper.getContext(context).acceptChat(chatId);
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
composePanel.setVisibility(View.VISIBLE);
|
||||
inputPanel.setVisibility(View.VISIBLE);
|
||||
});
|
||||
|
||||
if (dcChat.getType() == DcChat.DC_CHAT_TYPE_GROUP) {
|
||||
|
||||
@@ -462,7 +462,7 @@ public class ConversationItem extends BaseConversationItem {
|
||||
int end = spanned.getSpanEnd(span);
|
||||
if (start >= 0 && end > start && end <= spanned.length()) {
|
||||
String linkText = spanned.subSequence(start, end).toString();
|
||||
String label = context.getString(R.string.accessibility_link_action, linkText);
|
||||
String label = context.getString(R.string.open_link, linkText);
|
||||
linkActionIds.add(
|
||||
ViewCompat.addAccessibilityAction(
|
||||
this,
|
||||
@@ -1096,7 +1096,18 @@ public class ConversationItem extends BaseConversationItem {
|
||||
if (!messageRecord.isOutgoing() && callInfo.state instanceof CallState.Alerting) {
|
||||
int callId = messageRecord.getId();
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(context);
|
||||
coordinator.showIncomingCallScreen(callId);
|
||||
|
||||
if (coordinator.hasActiveCall()) {
|
||||
coordinator.showIncomingCallScreen(callId);
|
||||
} else {
|
||||
if (callInfo.sdpOffer == null) {
|
||||
Toast.makeText(context, R.string.error, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
int accId = dcContext.getAccountId();
|
||||
coordinator.handleIncomingCallFromConversation(
|
||||
accId, callId, callInfo.sdpOffer, callInfo.hasVideo);
|
||||
}
|
||||
} else {
|
||||
if (callInfo.hasVideo) {
|
||||
CallUtil.startVideoCall(getContext(), chatId);
|
||||
|
||||
@@ -607,6 +607,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
}
|
||||
}
|
||||
|
||||
public void handleQrFromSearch(String rawQrString) {
|
||||
qrData = rawQrString;
|
||||
new QrCodeHandler(this)
|
||||
.handleQrData(
|
||||
rawQrString, SecurejoinSource.Scan, SecurejoinUiPath.QrIcon, relayLockLauncher);
|
||||
}
|
||||
|
||||
private void handleResetRelaying() {
|
||||
resetRelayingMessageContent(this);
|
||||
refreshTitle();
|
||||
@@ -720,6 +727,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
refreshAvatar();
|
||||
refreshUnreadIndicator();
|
||||
refreshTitle();
|
||||
conversationListFragment.resetScrollPosition();
|
||||
conversationListFragment.loadChatlistAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -61,11 +60,11 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
private RecyclerView list;
|
||||
private View emptyState;
|
||||
private TextView emptySearch;
|
||||
private final String queryFilter = "";
|
||||
private boolean archive;
|
||||
private Timer reloadTimer;
|
||||
private boolean chatlistJustLoaded;
|
||||
private boolean reloadTimerInstantly;
|
||||
private boolean resetScrollPosition;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
@@ -242,9 +241,9 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
|
||||
public void loadChatlistAsync() {
|
||||
synchronized (loadChatlistLock) {
|
||||
needsAnotherLoad = true;
|
||||
if (inLoadChatlist) {
|
||||
Log.i(TAG, "chatlist loading debounced");
|
||||
needsAnotherLoad = true;
|
||||
return;
|
||||
}
|
||||
inLoadChatlist = true;
|
||||
@@ -253,6 +252,9 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
while (true) {
|
||||
Log.i(TAG, "executing debounced chatlist loading");
|
||||
loadChatlist();
|
||||
|
||||
synchronized (loadChatlistLock) {
|
||||
if (!needsAnotherLoad) {
|
||||
inLoadChatlist = false;
|
||||
@@ -261,8 +263,6 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
needsAnotherLoad = false;
|
||||
}
|
||||
|
||||
Log.i(TAG, "executing debounced chatlist loading");
|
||||
loadChatlist();
|
||||
Util.sleep(100);
|
||||
}
|
||||
});
|
||||
@@ -285,22 +285,17 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
Log.w(TAG, "Ignoring call to loadChatlist()");
|
||||
return;
|
||||
}
|
||||
DcChatlist chatlist =
|
||||
DcHelper.getContext(context)
|
||||
.getChatlist(listflags, queryFilter.isEmpty() ? null : queryFilter, 0);
|
||||
long startMs = System.currentTimeMillis();
|
||||
DcChatlist chatlist = DcHelper.getContext(context).getChatlist(listflags, null, 0);
|
||||
Log.i(TAG, "⏰ getChatlist(): " + (System.currentTimeMillis() - startMs) + "ms");
|
||||
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
if (chatlist.getCnt() <= 0 && TextUtils.isEmpty(queryFilter)) {
|
||||
if (chatlist.getCnt() <= 0) {
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.VISIBLE);
|
||||
emptySearch.setVisibility(View.INVISIBLE);
|
||||
fab.startPulse(3 * 1000);
|
||||
} else if (chatlist.getCnt() <= 0 && !TextUtils.isEmpty(queryFilter)) {
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
emptySearch.setVisibility(View.VISIBLE);
|
||||
emptySearch.setText(getString(R.string.search_no_result_for_x, queryFilter));
|
||||
} else {
|
||||
list.setVisibility(View.VISIBLE);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
@@ -309,6 +304,11 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
}
|
||||
|
||||
((ConversationListAdapter) list.getAdapter()).changeData(chatlist);
|
||||
|
||||
if (resetScrollPosition) {
|
||||
list.scrollToPosition(0);
|
||||
resetScrollPosition = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -365,4 +365,8 @@ public class ConversationListFragment extends BaseConversationListFragment
|
||||
loadChatlistAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public void resetScrollPosition() {
|
||||
resetScrollPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ import org.thoughtcrime.securesms.components.AvatarView;
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.search.QrInviteData;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -222,6 +224,40 @@ public class ConversationListItem extends RelativeLayout
|
||||
avatar.setSeenRecently(false);
|
||||
}
|
||||
|
||||
public void bind(@NonNull QrInviteData inviteData, @NonNull GlideRequests glideRequests) {
|
||||
this.selectedThreads = Collections.emptySet();
|
||||
|
||||
fromView.setText(inviteData.getDisplayTitle());
|
||||
fromView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
subjectView.setVisibility(VISIBLE);
|
||||
subjectView.setText(inviteData.getDisplaySubtitle());
|
||||
subjectView.setTypeface(LIGHT_TYPEFACE);
|
||||
subjectView.setTextColor(
|
||||
ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color));
|
||||
|
||||
dateView.setText("");
|
||||
dateView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
archivedBadgeView.setVisibility(GONE);
|
||||
requestBadgeView.setVisibility(GONE);
|
||||
unreadIndicator.setVisibility(GONE);
|
||||
deliveryStatusIndicator.setNone();
|
||||
|
||||
setBatchState(false);
|
||||
|
||||
if (inviteData.getContactId() > 0) {
|
||||
DcContext dcContext = DcHelper.getContext(getContext());
|
||||
DcContact contact = dcContext.getContact(inviteData.getContactId());
|
||||
Recipient recipient = new Recipient(getContext(), contact);
|
||||
avatar.setAvatar(glideRequests, recipient, false);
|
||||
avatar.setSeenRecently(contact.wasSeenRecently());
|
||||
} else {
|
||||
avatar.setImageDrawable(
|
||||
new GeneratedContactPhoto("+")
|
||||
.asDrawable(getContext(), ThemeUtil.getDummyContactColor(getContext())));
|
||||
avatar.setSeenRecently(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbind() {}
|
||||
|
||||
|
||||
@@ -33,14 +33,13 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMediaGalleryElement;
|
||||
@@ -48,7 +47,6 @@ import com.b44t.messenger.DcMsg;
|
||||
import java.io.IOException;
|
||||
import java.util.WeakHashMap;
|
||||
import org.thoughtcrime.securesms.components.MediaView;
|
||||
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||
@@ -88,7 +86,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
@Nullable private DcMsg messageRecord;
|
||||
private DcContext dcContext;
|
||||
private MediaItem initialMedia;
|
||||
private ViewPager mediaPager;
|
||||
private ViewPager2 mediaPager;
|
||||
private Recipient conversationRecipient;
|
||||
private boolean leftIsRecent;
|
||||
|
||||
@@ -190,7 +188,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
private void initializeViews() {
|
||||
mediaPager = findViewById(R.id.media_pager);
|
||||
mediaPager.setOffscreenPageLimit(1);
|
||||
mediaPager.addOnPageChangeListener(new ViewPagerListener());
|
||||
mediaPager.registerOnPageChangeCallback(new ViewPagerListener());
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
@@ -260,10 +258,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private int cleanupMedia() {
|
||||
int restartItem = mediaPager.getCurrentItem();
|
||||
|
||||
mediaPager.removeAllViews();
|
||||
mediaPager.setAdapter(null);
|
||||
|
||||
return restartItem;
|
||||
}
|
||||
|
||||
@@ -483,22 +478,25 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
DcMediaPagerAdapter adapter =
|
||||
new DcMediaPagerAdapter(this, GlideApp.with(this), getWindow(), data, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
mediaPager.setAdapter(adapter);
|
||||
|
||||
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition());
|
||||
else mediaPager.setCurrentItem(restartItem);
|
||||
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition(), false);
|
||||
else mediaPager.setCurrentItem(restartItem, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<DcMediaGalleryElement> loader) {}
|
||||
|
||||
private class ViewPagerListener extends ExtendedOnPageChangedListener {
|
||||
private class ViewPagerListener extends ViewPager2.OnPageChangeCallback {
|
||||
|
||||
private Integer currentPage = null;
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
super.onPageSelected(position);
|
||||
if (currentPage != null && currentPage != position) onPageUnselected(currentPage);
|
||||
currentPage = position;
|
||||
|
||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
||||
|
||||
@@ -510,8 +508,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageUnselected(int position) {
|
||||
private void onPageUnselected(int position) {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
@@ -526,7 +523,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter {
|
||||
private static class SingleItemPagerAdapter
|
||||
extends RecyclerView.Adapter<SingleItemPagerAdapter.MediaViewHolder>
|
||||
implements MediaItemAdapter {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final Window window;
|
||||
@@ -555,37 +554,29 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new MediaViewHolder(inflater.inflate(R.layout.media_view_page, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
View itemView = inflater.inflate(R.layout.media_view_page, container, false);
|
||||
MediaView mediaView = itemView.findViewById(R.id.media_view);
|
||||
|
||||
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
|
||||
try {
|
||||
mediaView.set(glideRequests, window, uri, name, mediaType, size, true);
|
||||
holder.mediaView.set(glideRequests, window, uri, name, mediaType, size, true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
container.addView(itemView);
|
||||
|
||||
return itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
|
||||
mediaView.cleanup();
|
||||
|
||||
container.removeView((FrameLayout) object);
|
||||
public void onViewRecycled(@NonNull MediaViewHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
holder.mediaView.cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -595,9 +586,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void pause(int position) {}
|
||||
|
||||
static class MediaViewHolder extends RecyclerView.ViewHolder {
|
||||
final MediaView mediaView;
|
||||
|
||||
MediaViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
mediaView = itemView.findViewById(R.id.media_view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class DcMediaPagerAdapter extends PagerAdapter implements MediaItemAdapter {
|
||||
private static class DcMediaPagerAdapter
|
||||
extends RecyclerView.Adapter<DcMediaPagerAdapter.MediaViewHolder>
|
||||
implements MediaItemAdapter {
|
||||
|
||||
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
|
||||
|
||||
@@ -630,21 +632,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
if (!active) return 0;
|
||||
else return gallery.getCount();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View itemView = LayoutInflater.from(context).inflate(R.layout.media_view_page, parent, false);
|
||||
return new MediaViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
View itemView =
|
||||
LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false);
|
||||
MediaView mediaView = itemView.findViewById(R.id.media_view);
|
||||
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
|
||||
boolean autoplay = position == autoPlayPosition;
|
||||
int cursorPosition = getCursorPosition(position);
|
||||
|
||||
@@ -656,7 +657,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
mediaView.set(
|
||||
holder.mediaView.set(
|
||||
glideRequests,
|
||||
window,
|
||||
Uri.fromFile(msg.getFileAsFile()),
|
||||
@@ -668,19 +669,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
mediaViews.put(position, mediaView);
|
||||
container.addView(itemView);
|
||||
|
||||
return itemView;
|
||||
mediaViews.put(position, holder.mediaView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
|
||||
mediaView.cleanup();
|
||||
|
||||
mediaViews.remove(position);
|
||||
container.removeView((FrameLayout) object);
|
||||
public void onViewRecycled(@NonNull MediaViewHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
int pos = holder.getBindingAdapterPosition();
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
mediaViews.remove(pos);
|
||||
}
|
||||
holder.mediaView.cleanup();
|
||||
}
|
||||
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
@@ -710,6 +709,15 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
if (leftIsRecent) return position;
|
||||
else return gallery.getCount() - 1 - position;
|
||||
}
|
||||
|
||||
static class MediaViewHolder extends RecyclerView.ViewHolder {
|
||||
final MediaView mediaView;
|
||||
|
||||
MediaViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
mediaView = itemView.findViewById(R.id.media_view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class MediaItem {
|
||||
@@ -742,7 +750,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
interface MediaItemAdapter {
|
||||
private interface MediaItemAdapter {
|
||||
MediaItem getMediaItemFor(int position);
|
||||
|
||||
void pause(int position);
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -25,8 +26,11 @@ import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
@@ -36,6 +40,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
public class ProfileActivity extends PassphraseRequiredActionBarActivity
|
||||
implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
private static final String TAG = "ProfileActivity";
|
||||
|
||||
public static final String CHAT_ID_EXTRA = "chat_id";
|
||||
public static final String CONTACT_ID_EXTRA = "contact_id";
|
||||
|
||||
@@ -398,7 +404,18 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
|
||||
Intent composeIntent = new Intent();
|
||||
DcContact dcContact = dcContext.getContact(contactId);
|
||||
if (dcContact.isKeyContact()) {
|
||||
ShareUtil.setSharedContactId(composeIntent, contactId);
|
||||
try {
|
||||
byte[] vcard =
|
||||
rpc.makeVcard(rpc.getSelectedAccountId(), Collections.singletonList(contactId))
|
||||
.getBytes();
|
||||
Uri vcardUri =
|
||||
PersistentBlobProvider.getInstance().create(this, vcard, "text/vcard", "contact.vcf");
|
||||
ArrayList<Uri> uris = new ArrayList<>();
|
||||
uris.add(vcardUri);
|
||||
ShareUtil.setSharedUris(composeIntent, uris);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to create vCard for sharing contactId=" + contactId, e);
|
||||
}
|
||||
} else {
|
||||
ShareUtil.setSharedText(composeIntent, dcContact.getAddr());
|
||||
}
|
||||
|
||||
@@ -260,12 +260,11 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||
final String addr = extraEmail[0];
|
||||
int contactId = dcContext.lookupContactIdByAddr(addr);
|
||||
|
||||
if (contactId == 0) {
|
||||
contactId = dcContext.createContact(null, addr);
|
||||
if (contactId != 0 || dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
|
||||
if (contactId == 0) contactId = dcContext.createContact(null, addr);
|
||||
chatId = dcContext.createChatByContactId(contactId);
|
||||
accId = dcContext.getAccountId();
|
||||
}
|
||||
|
||||
chatId = dcContext.createChatByContactId(contactId);
|
||||
accId = dcContext.getAccountId();
|
||||
}
|
||||
Intent composeIntent;
|
||||
if (accId != -1 && chatId > 0) {
|
||||
|
||||
@@ -68,6 +68,11 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean allowInLockedMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
setContentView(R.layout.web_view_activity);
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import chat.delta.rpc.types.WebxdcMessageInfo;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
@@ -50,7 +51,6 @@ import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
@@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
|
||||
private static final String TAG = "WebxdcActivity";
|
||||
private static final String EXTRA_ACCOUNT_ID = "accountId";
|
||||
private static final String EXTRA_CHAT_ID = "chatId";
|
||||
private static final String EXTRA_APP_MSG_ID = "appMessageId";
|
||||
private static final String EXTRA_HIDE_ACTION_BAR = "hideActionBar";
|
||||
private static final String EXTRA_HREF = "href";
|
||||
@@ -69,11 +70,15 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
|
||||
private ValueCallback<Uri[]> filePathCallback;
|
||||
private DcContext dcContext;
|
||||
private int accountId;
|
||||
private Rpc rpc;
|
||||
private int chatId;
|
||||
private DcMsg dcAppMsg;
|
||||
private String baseURL;
|
||||
private String sourceCodeUrl = "";
|
||||
private String selfAddr;
|
||||
private boolean isAppSender;
|
||||
private boolean isBroadcast;
|
||||
private int sendUpdateMaxSize;
|
||||
private int sendUpdateInterval;
|
||||
private boolean internetAccess = false;
|
||||
@@ -109,7 +114,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
}
|
||||
dcContext.setConfigInt("ui.maps_version", mapsVersion);
|
||||
}
|
||||
openWebxdcActivity(context, msgId, true, href);
|
||||
openWebxdcActivity(context, msgId, chatId, true, href);
|
||||
}
|
||||
|
||||
public static void openWebxdcActivity(Context context, DcMsg instance) {
|
||||
@@ -117,37 +122,43 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
}
|
||||
|
||||
public static void openWebxdcActivity(Context context, @NonNull DcMsg instance, String href) {
|
||||
openWebxdcActivity(context, instance.getId(), false, href);
|
||||
openWebxdcActivity(context, instance.getId(), instance.getChatId(), false, href);
|
||||
}
|
||||
|
||||
public static void openWebxdcActivity(
|
||||
Context context, int msgId, boolean hideActionBar, String href) {
|
||||
Context context, int msgId, int chatId, boolean hideActionBar, String href) {
|
||||
if (!Util.isClickedRecently()) {
|
||||
context.startActivity(getWebxdcIntent(context, msgId, hideActionBar, href));
|
||||
context.startActivity(getWebxdcIntent(context, msgId, chatId, hideActionBar, href));
|
||||
}
|
||||
}
|
||||
|
||||
private static Intent getWebxdcIntent(
|
||||
Context context, int msgId, boolean hideActionBar, String href) {
|
||||
Context context, int msgId, int chatId, boolean hideActionBar, String href) {
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
int accountId = dcContext.getAccountId();
|
||||
Intent intent = new Intent(context, WebxdcActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(EXTRA_ACCOUNT_ID, dcContext.getAccountId());
|
||||
// Unique URI per webxdc instance so FLAG_ACTIVITY_NEW_DOCUMENT can identify the document:
|
||||
intent.setData(Uri.parse("webxdc://" + accountId + "/" + msgId));
|
||||
intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
|
||||
intent.putExtra(EXTRA_CHAT_ID, chatId);
|
||||
intent.putExtra(EXTRA_APP_MSG_ID, msgId);
|
||||
intent.putExtra(EXTRA_HIDE_ACTION_BAR, hideActionBar);
|
||||
intent.putExtra(EXTRA_HREF, href);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private static Intent[] getWebxdcIntentWithParentStack(Context context, int msgId) {
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
|
||||
int chatId = dcContext.getMsg(msgId).getChatId();
|
||||
final Intent chatIntent =
|
||||
new Intent(context, ConversationActivity.class)
|
||||
.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId())
|
||||
.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId)
|
||||
.setAction(Intent.ACTION_VIEW);
|
||||
|
||||
final Intent webxdcIntent = getWebxdcIntent(context, msgId, false, "");
|
||||
final Intent webxdcIntent = getWebxdcIntent(context, msgId, chatId, false, "");
|
||||
|
||||
return TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(chatIntent)
|
||||
@@ -193,18 +204,11 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
}
|
||||
});
|
||||
|
||||
DcEventCenter eventCenter =
|
||||
DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this);
|
||||
|
||||
int appMessageId = b.getInt(EXTRA_APP_MSG_ID);
|
||||
int accountId = b.getInt(EXTRA_ACCOUNT_ID);
|
||||
accountId = b.getInt(EXTRA_ACCOUNT_ID);
|
||||
this.dcContext = DcHelper.getContext(getApplicationContext());
|
||||
if (accountId != dcContext.getAccountId()) {
|
||||
AccountManager.getInstance().switchAccount(getApplicationContext(), accountId);
|
||||
this.dcContext = DcHelper.getContext(getApplicationContext());
|
||||
this.dcContext = DcHelper.getAccounts(getApplicationContext()).getAccount(accountId);
|
||||
}
|
||||
|
||||
this.dcAppMsg = this.dcContext.getMsg(appMessageId);
|
||||
@@ -213,6 +217,15 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
chatId = b.getInt(EXTRA_CHAT_ID, dcAppMsg.getChatId());
|
||||
|
||||
DcEventCenter eventCenter =
|
||||
DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_MSG_DELETED, this);
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_DELETED, this);
|
||||
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this);
|
||||
|
||||
// `msg_id` in the subdomain makes sure, different apps using same files do not share the same
|
||||
// cache entry
|
||||
@@ -220,21 +233,32 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
// (a random-id would also work, but would need maintenance and does not add benefits as we
|
||||
// regard the file-part interceptRequest() only,
|
||||
// also a random-id is not that useful for debugging)
|
||||
this.baseURL = "https://acc" + dcContext.getAccountId() + "-msg" + appMessageId + ".localhost";
|
||||
this.baseURL = "https://acc" + accountId + "-msg" + appMessageId + ".localhost";
|
||||
|
||||
final JSONObject info = this.dcAppMsg.getWebxdcInfo();
|
||||
internetAccess = JsonUtils.optBoolean(info, "internet_access");
|
||||
if ("landscape".equals(JsonUtils.optString(info, "orientation"))) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} else {
|
||||
// enter fullscreen mode if necessary,
|
||||
// this is needed here because if the app is opened while already in landscape mode,
|
||||
// onConfigurationChanged() is not triggered
|
||||
setScreenMode(getResources().getConfiguration());
|
||||
WebxdcMessageInfo info;
|
||||
try {
|
||||
info = rpc.getWebxdcInfo(accountId, appMessageId);
|
||||
|
||||
if ("landscape".equals(info.orientation)) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} else {
|
||||
// enter fullscreen mode if necessary,
|
||||
// this is needed here because if the app is opened while already in landscape mode,
|
||||
// onConfigurationChanged() is not triggered
|
||||
setScreenMode(getResources().getConfiguration());
|
||||
}
|
||||
|
||||
internetAccess = info.internetAccess;
|
||||
selfAddr = info.selfAddr;
|
||||
isAppSender = info.isAppSender;
|
||||
isBroadcast = info.isBroadcast;
|
||||
sendUpdateMaxSize = info.sendUpdateMaxSize;
|
||||
sendUpdateInterval = info.sendUpdateInterval;
|
||||
} catch (RpcException e) { // unexpected error, log it and finish
|
||||
Log.e(TAG, "RPC Error", e);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
selfAddr = info.optString("self_addr");
|
||||
sendUpdateMaxSize = info.optInt("send_update_max_size");
|
||||
sendUpdateInterval = info.optInt("send_update_interval");
|
||||
|
||||
toggleFakeProxy(!internetAccess);
|
||||
|
||||
@@ -315,7 +339,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
super.onOptionsItemSelected(item);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.menu_add_to_home_screen) {
|
||||
addToHomeScreen(this, dcAppMsg.getId());
|
||||
addToHomeScreen(this, dcContext, dcAppMsg.getId());
|
||||
return true;
|
||||
} else if (itemId == R.id.webxdc_help) {
|
||||
DcHelper.openHelp(this, "#webxdc");
|
||||
@@ -468,6 +492,8 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
|
||||
@Override
|
||||
public void handleEvent(@NonNull DcEvent event) {
|
||||
if (event.getAccountId() != accountId) return;
|
||||
|
||||
int eventId = event.getId();
|
||||
if ((eventId == DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE
|
||||
&& event.getData1Int() == dcAppMsg.getId())) {
|
||||
@@ -487,22 +513,26 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
this.dcContext.getMsg(event.getData2Int()); // msg changed, reload data from db
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
final JSONObject info = dcAppMsg.getWebxdcInfo();
|
||||
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
updateTitleAndMenu(info, chat);
|
||||
});
|
||||
try {
|
||||
final WebxdcMessageInfo info =
|
||||
rpc.getWebxdcInfo(dcContext.getAccountId(), dcAppMsg.getId());
|
||||
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
|
||||
Util.runOnMain(() -> updateTitleAndMenu(info, chat));
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC Error", e);
|
||||
}
|
||||
});
|
||||
} else if ((eventId == DcContext.DC_EVENT_MSG_DELETED
|
||||
&& event.getData2Int() == dcAppMsg.getId())
|
||||
|| (eventId == DcContext.DC_EVENT_CHAT_DELETED && event.getData1Int() == chatId)) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTitleAndMenu(JSONObject info, DcChat chat) {
|
||||
final String docName = JsonUtils.optString(info, "document");
|
||||
final String xdcName = JsonUtils.optString(info, "name");
|
||||
final String currSourceCodeUrl = JsonUtils.optString(info, "source_code_url");
|
||||
getSupportActionBar()
|
||||
.setTitle((docName.isEmpty() ? xdcName : docName) + " – " + chat.getName());
|
||||
private void updateTitleAndMenu(WebxdcMessageInfo info, DcChat chat) {
|
||||
final String docName = TextUtils.isEmpty(info.document) ? info.name : info.document;
|
||||
getSupportActionBar().setTitle(docName + " – " + chat.getName());
|
||||
String currSourceCodeUrl = info.sourceCodeUrl != null ? info.sourceCodeUrl : "";
|
||||
if (!sourceCodeUrl.equals(currSourceCodeUrl)) {
|
||||
sourceCodeUrl = currSourceCodeUrl;
|
||||
invalidateOptionsMenu();
|
||||
@@ -511,6 +541,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
|
||||
private void showInChat() {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accountId);
|
||||
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcAppMsg.getChatId());
|
||||
intent.putExtra(
|
||||
ConversationActivity.STARTING_POSITION_EXTRA,
|
||||
@@ -519,35 +550,53 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
}
|
||||
|
||||
public static void addToHomeScreen(Activity activity, int msgId) {
|
||||
addToHomeScreen(activity, DcHelper.getContext(activity), msgId);
|
||||
}
|
||||
|
||||
public static void addToHomeScreen(Activity activity, DcContext dcContext, int msgId) {
|
||||
Context context = activity.getApplicationContext();
|
||||
try {
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
Rpc rpc = DcHelper.getRpc(context);
|
||||
int accountId = dcContext.getAccountId();
|
||||
DcMsg msg = dcContext.getMsg(msgId);
|
||||
final JSONObject info = msg.getWebxdcInfo();
|
||||
WebxdcMessageInfo info = rpc.getWebxdcInfo(accountId, msgId);
|
||||
|
||||
final String docName = JsonUtils.optString(info, "document");
|
||||
final String xdcName = JsonUtils.optString(info, "name");
|
||||
byte[] blob = msg.getWebxdcBlob(JsonUtils.optString(info, "icon"));
|
||||
final String docName = TextUtils.isEmpty(info.document) ? info.name : info.document;
|
||||
byte[] blob = msg.getWebxdcBlob(info.icon);
|
||||
ByteArrayInputStream is = new ByteArrayInputStream(blob);
|
||||
BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon");
|
||||
Bitmap bitmap = drawable.getBitmap();
|
||||
|
||||
ShortcutInfoCompat shortcutInfoCompat =
|
||||
new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId)
|
||||
.setShortLabel(docName.isEmpty() ? xdcName : docName)
|
||||
new ShortcutInfoCompat.Builder(context, "xdc-" + accountId + "-" + msgId)
|
||||
.setShortLabel(docName)
|
||||
.setIcon(
|
||||
IconCompat.createWithBitmap(
|
||||
bitmap)) // createWithAdaptiveBitmap() removes decorations but cuts out a too
|
||||
// small circle and defamiliarize the icon too much
|
||||
.setIntents(getWebxdcIntentWithParentStack(context, msgId))
|
||||
.setIntent(getWebxdcIntent(context, msgId, msg.getChatId(), false, ""))
|
||||
.build();
|
||||
|
||||
Toast.makeText(context, R.string.one_moment, Toast.LENGTH_SHORT).show();
|
||||
if (!ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) {
|
||||
Toast.makeText(
|
||||
context, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
boolean success;
|
||||
try {
|
||||
success = ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "ErrAddToHomescreen: requestPinShortcut() failed", e);
|
||||
success = false;
|
||||
}
|
||||
if (!success) {
|
||||
Util.runOnMain(
|
||||
() ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
"ErrAddToHomescreen: requestPinShortcut() failed",
|
||||
Toast.LENGTH_LONG)
|
||||
.show());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(context, "ErrAddToHomescreen: " + e, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
@@ -617,6 +666,16 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
return WebxdcActivity.this.dcContext.getName();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean isAppSender() {
|
||||
return WebxdcActivity.this.isAppSender;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean isBroadcast() {
|
||||
return WebxdcActivity.this.isBroadcast;
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.AnimatorInflater;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -71,6 +75,11 @@ public class WelcomeActivity extends BaseActionBarActivity
|
||||
.setOnClickListener((v) -> showSignInDialogWithPermission());
|
||||
findViewById(R.id.backup_button).setOnClickListener((v) -> startImportBackup());
|
||||
|
||||
AnimatorSet floating =
|
||||
(AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.floating_logo);
|
||||
floating.setTarget(findViewById(R.id.welcome_icon));
|
||||
floating.start();
|
||||
|
||||
registerForEvents();
|
||||
initializeActionBar();
|
||||
|
||||
@@ -120,7 +129,15 @@ public class WelcomeActivity extends BaseActionBarActivity
|
||||
|
||||
boolean canGoBack = AccountManager.getInstance().canRollbackAccountCreation(this);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(canGoBack);
|
||||
getSupportActionBar().setTitle(canGoBack ? R.string.add_account : R.string.app_name);
|
||||
if (canGoBack) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
supportActionBar.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||
supportActionBar.setElevation(0);
|
||||
}
|
||||
supportActionBar.setTitle(R.string.add_account);
|
||||
} else {
|
||||
supportActionBar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void registerForEvents() {
|
||||
@@ -189,7 +206,12 @@ public class WelcomeActivity extends BaseActionBarActivity
|
||||
File imexDir = DcHelper.getImexDir();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AttachmentManager.selectMediaType(
|
||||
this, "application/x-tar", null, PICK_BACKUP, StorageUtil.getDownloadUri());
|
||||
this,
|
||||
"application/x-tar",
|
||||
null,
|
||||
PICK_BACKUP,
|
||||
StorageUtil.getDownloadUri(),
|
||||
false);
|
||||
} else {
|
||||
final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath());
|
||||
if (backupFile != null) {
|
||||
|
||||
@@ -4,15 +4,10 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public abstract class Attachment {
|
||||
|
||||
@@ -116,14 +111,7 @@ public abstract class Attachment {
|
||||
filename = filename.substring(0, i);
|
||||
}
|
||||
}
|
||||
String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext);
|
||||
|
||||
// copy content to this file
|
||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, getDataUri());
|
||||
OutputStream outputStream = new FileOutputStream(path);
|
||||
Util.copy(inputStream, outputStream);
|
||||
|
||||
return path;
|
||||
return DcHelper.copyToBlobdir(context, getDataUri(), filename, ext);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.calls;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import org.thoughtcrime.securesms.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public class CallActionReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "CallActionReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) return;
|
||||
|
||||
String action = intent.getAction();
|
||||
Log.d(TAG, "Received action: " + action);
|
||||
|
||||
if (CallActivity.ACTION_DECLINE_CALL.equals(action)) {
|
||||
CallCoordinator.getInstance(context).declineCall();
|
||||
} else if (CallActivity.ACTION_HANGUP_CALL.equals(action)) {
|
||||
CallCoordinator.getInstance(context).hangUp();
|
||||
} else if (CallActivity.ACTION_CALL_BACK.equals(action)) {
|
||||
int chatId = intent.getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
|
||||
int accId = intent.getIntExtra(ConversationActivity.ACCOUNT_ID_EXTRA, -1);
|
||||
boolean video = intent.getBooleanExtra(CallActivity.EXTRA_STARTS_WITH_VIDEO, false);
|
||||
if (chatId > 0 && accId > 0) {
|
||||
NotificationManagerCompat.from(context).cancel(CallCoordinator.NOTIFICATION_ID_MISSED_CALL);
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(context);
|
||||
if (coordinator.hasActiveCall()) {
|
||||
Toast.makeText(context, R.string.already_in_call, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
coordinator.initiateOutgoingCall(accId, chatId, video);
|
||||
}
|
||||
}
|
||||
} else if (CallActivity.ACTION_MESSAGE.equals(action)) {
|
||||
int chatId = intent.getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
|
||||
int accId = intent.getIntExtra(ConversationActivity.ACCOUNT_ID_EXTRA, -1);
|
||||
NotificationManagerCompat.from(context).cancel(CallCoordinator.NOTIFICATION_ID_MISSED_CALL);
|
||||
if (chatId > 0 && accId > 0) {
|
||||
Intent convIntent = new Intent(context, ConversationActivity.class);
|
||||
convIntent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
|
||||
convIntent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
|
||||
convIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
context.startActivity(convIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,13 @@ import android.app.PictureInPictureParams;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
import android.view.View;
|
||||
@@ -22,7 +24,9 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
@@ -54,10 +58,14 @@ public class CallActivity extends AppCompatActivity {
|
||||
private static final String TAG = "CallActivity";
|
||||
private static final int MIC_PERMISSION_REQUEST_CODE = 1001;
|
||||
private static final int CAMERA_PERMISSION_REQUEST_CODE = 1002;
|
||||
private static final int CAMERA_MID_CALL_PERMISSION_REQUEST_CODE = 1003;
|
||||
|
||||
public static final String ACTION_ANSWER_CALL = BuildConfig.APPLICATION_ID + ".ANSWER_CALL";
|
||||
public static final String ACTION_DECLINE_CALL = BuildConfig.APPLICATION_ID + ".DECLINE_CALL";
|
||||
public static final String ACTION_HANGUP_CALL = BuildConfig.APPLICATION_ID + ".HANGUP_CALL";
|
||||
public static final String ACTION_CALL_BACK = BuildConfig.APPLICATION_ID + ".CALL_BACK";
|
||||
public static final String ACTION_MESSAGE = BuildConfig.APPLICATION_ID + ".MESSAGE";
|
||||
public static final String EXTRA_STARTS_WITH_VIDEO = "starts_with_video";
|
||||
|
||||
// Views
|
||||
|
||||
@@ -99,6 +107,7 @@ public class CallActivity extends AppCompatActivity {
|
||||
private boolean awaitingPermissionResult = false;
|
||||
private boolean pausedWhileAwaitingPermission = false;
|
||||
private boolean intentHandled = false;
|
||||
private boolean doNotAutoFinish = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -227,6 +236,7 @@ public class CallActivity extends AppCompatActivity {
|
||||
Log.d(TAG, "Resuming existing call");
|
||||
} else if (!coordinator.isIncomingCall()) {
|
||||
Log.d(TAG, "Starting outgoing call");
|
||||
coordinator.ensureServiceStarted();
|
||||
viewModel.startOutgoingCallWhenReady();
|
||||
}
|
||||
}
|
||||
@@ -374,6 +384,18 @@ public class CallActivity extends AppCompatActivity {
|
||||
videoButton.setOnClickListener(
|
||||
v -> {
|
||||
if (viewModel != null) {
|
||||
Boolean currentEnabled = viewModel.getVideoEnabled().getValue();
|
||||
boolean needToEnable = currentEnabled == null || !currentEnabled;
|
||||
|
||||
if (needToEnable && !hasCameraPermission()) {
|
||||
awaitingPermissionResult = true;
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
new String[] {Manifest.permission.CAMERA},
|
||||
CAMERA_MID_CALL_PERMISSION_REQUEST_CODE);
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel.toggleVideo();
|
||||
}
|
||||
});
|
||||
@@ -634,7 +656,7 @@ public class CallActivity extends AppCompatActivity {
|
||||
new Handler(Looper.getMainLooper())
|
||||
.postDelayed(
|
||||
() -> {
|
||||
if (!isFinishing()) {
|
||||
if (!isFinishing() && !doNotAutoFinish) {
|
||||
finish();
|
||||
}
|
||||
},
|
||||
@@ -643,7 +665,9 @@ public class CallActivity extends AppCompatActivity {
|
||||
|
||||
case ENDED:
|
||||
statusText.setText(R.string.call_ended);
|
||||
finish();
|
||||
if (!doNotAutoFinish) {
|
||||
finish();
|
||||
}
|
||||
break;
|
||||
|
||||
case ERROR:
|
||||
@@ -656,7 +680,7 @@ public class CallActivity extends AppCompatActivity {
|
||||
new Handler(Looper.getMainLooper())
|
||||
.postDelayed(
|
||||
() -> {
|
||||
if (!isFinishing()) {
|
||||
if (!isFinishing() && !doNotAutoFinish) {
|
||||
finish();
|
||||
}
|
||||
},
|
||||
@@ -842,16 +866,28 @@ public class CallActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void handleMicPermissionDenied() {
|
||||
Toast.makeText(this, R.string.call_requires_mic_permission, Toast.LENGTH_LONG).show();
|
||||
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
|
||||
if (coordinator.hasActiveCall()
|
||||
&& coordinator.isIncomingCall()
|
||||
&& !coordinator.hasOngoingCall()) {
|
||||
coordinator.declineCall();
|
||||
|
||||
if (coordinator.hasActiveCall() && !coordinator.hasOngoingCall()) {
|
||||
if (coordinator.isIncomingCall()) {
|
||||
coordinator.declineCall();
|
||||
} else {
|
||||
coordinator.hangUp();
|
||||
}
|
||||
}
|
||||
|
||||
finish();
|
||||
if (!shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
|
||||
doNotAutoFinish = true;
|
||||
showPermissionSettingsDialog(
|
||||
getString(R.string.call_requires_mic_permission),
|
||||
() -> {
|
||||
doNotAutoFinish = false;
|
||||
if (!isFinishing()) finish();
|
||||
});
|
||||
} else {
|
||||
Toast.makeText(this, R.string.call_requires_mic_permission, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void proceedAfterPermissions() {
|
||||
@@ -878,6 +914,22 @@ public class CallActivity extends AppCompatActivity {
|
||||
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
|
||||
|
||||
if (requestCode == CAMERA_MID_CALL_PERMISSION_REQUEST_CODE) {
|
||||
boolean cameraGranted =
|
||||
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (cameraGranted && viewModel != null) {
|
||||
viewModel.toggleVideo();
|
||||
} else {
|
||||
if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
|
||||
showPermissionSettingsDialog(getString(R.string.call_requires_camera_permission), null);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.call_requires_camera_permission, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestCode == MIC_PERMISSION_REQUEST_CODE) {
|
||||
boolean micGranted =
|
||||
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
@@ -893,9 +945,17 @@ public class CallActivity extends AppCompatActivity {
|
||||
|
||||
if (!cameraGranted) {
|
||||
Log.w(TAG, "Camera permission denied, switching to audio-only");
|
||||
Toast.makeText(
|
||||
this, "Starting audio-only call (camera permission denied)", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"Camera permission permanently denied. Enable in app settings for video calls.",
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this, "Starting audio-only call (camera permission denied)", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
coordinator.setStartsWithVideo(false);
|
||||
}
|
||||
}
|
||||
@@ -903,6 +963,34 @@ public class CallActivity extends AppCompatActivity {
|
||||
proceedAfterPermissions();
|
||||
}
|
||||
|
||||
private void showPermissionSettingsDialog(String message, @Nullable Runnable onDismissAction) {
|
||||
if (isFinishing() || isDestroyed()) {
|
||||
if (onDismissAction != null) onDismissAction.run();
|
||||
return;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Permission Required")
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
"Open Settings",
|
||||
(dialog, which) -> {
|
||||
Intent intent =
|
||||
new Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", getPackageName(), null));
|
||||
startActivity(intent);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setOnDismissListener(
|
||||
dialog -> {
|
||||
if (onDismissAction != null) {
|
||||
onDismissAction.run();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
// Picture-in-Picture
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.os.Looper;
|
||||
import android.telecom.DisconnectCause;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -32,6 +33,7 @@ import androidx.core.telecom.CallControlScope;
|
||||
import androidx.core.telecom.CallEndpointCompat;
|
||||
import androidx.core.telecom.CallException;
|
||||
import androidx.core.telecom.CallsManager;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.lifecycle.FlowLiveDataConversions;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
@@ -55,6 +57,7 @@ import kotlinx.coroutines.Dispatchers;
|
||||
import kotlinx.coroutines.flow.Flow;
|
||||
import kotlinx.coroutines.flow.FlowKt;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -68,7 +71,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
// Notification channels
|
||||
private static final String CHANNEL_ID_INCOMING = "voip_incoming_calls";
|
||||
private static final String CHANNEL_ID_ONGOING = "voip_ongoing_calls";
|
||||
private static final String CHANNEL_ID_MISSED = "voip_missed_calls";
|
||||
private static final int NOTIFICATION_ID_CALL = 1001;
|
||||
static final int NOTIFICATION_ID_MISSED_CALL = 1002;
|
||||
|
||||
private static final int PI_ANSWER = 0;
|
||||
private static final int PI_DECLINE = 1;
|
||||
private static final int PI_FULLSCREEN = 2;
|
||||
private static final int PI_HANGUP = 3;
|
||||
private static final int PI_ONGOING_CONTENT = 4;
|
||||
private static final int PI_MISSED_CONTENT = 5;
|
||||
private static final int PI_MISSED_CALLBACK = 6;
|
||||
private static final int PI_MISSED_MESSAGE = 7;
|
||||
|
||||
private static final String CALL_IDENTIFIER_SCHEME = "deltachat:";
|
||||
|
||||
@@ -97,6 +111,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
private final MutableLiveData<Boolean> outgoingCallPlaced = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> answeredElsewhere = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> isFrontCamera = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<Boolean> mediaCaptureReady = new MutableLiveData<>(false);
|
||||
|
||||
// Audio Routing Support
|
||||
private final MediatorLiveData<CallEndpointCompat> currentAudioEndpoint =
|
||||
@@ -119,6 +134,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
private String pendingOfferSdp;
|
||||
private boolean hasNotifiedBackend = false;
|
||||
private boolean hasAutoSelectedEarpiece = false;
|
||||
private boolean pendingMediaCapture = false;
|
||||
private boolean wasAnsweredLocally = false;
|
||||
|
||||
private CallControlScope activeCallControlScope;
|
||||
private CallViewModel activeCallViewModel;
|
||||
@@ -165,8 +182,14 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
ongoingChannel.setDescription("Notifications for active DeltaChat calls");
|
||||
ongoingChannel.setSound(null, null);
|
||||
|
||||
NotificationChannel missedChannel =
|
||||
new NotificationChannel(
|
||||
CHANNEL_ID_MISSED, "Missed Calls", NotificationManager.IMPORTANCE_HIGH);
|
||||
missedChannel.setDescription("Notifications for missed DeltaChat calls");
|
||||
|
||||
notificationManager.createNotificationChannel(incomingChannel);
|
||||
notificationManager.createNotificationChannel(ongoingChannel);
|
||||
notificationManager.createNotificationChannel(missedChannel);
|
||||
}
|
||||
|
||||
private void registerTelecom() {
|
||||
@@ -180,6 +203,29 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private void disconnectTelecom(DisconnectCause cause) {
|
||||
CallControlScope scope = activeCallControlScope;
|
||||
if (scope == null) {
|
||||
Log.d(TAG, "No active CallControlScope, skipping disconnect");
|
||||
return;
|
||||
}
|
||||
|
||||
scope.disconnect(
|
||||
cause,
|
||||
new Continuation<CallControlResult>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public CoroutineContext getContext() {
|
||||
return EmptyCoroutineContext.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeWith(@NonNull Object result) {
|
||||
Log.d(TAG, "Telecom disconnect completed: " + result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addEventListeners() {
|
||||
DcEventCenter eventCenter = DcHelper.getEventCenter(this.appContext);
|
||||
eventCenter.removeObservers(this);
|
||||
@@ -228,6 +274,11 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
mainHandler.removeCallbacks(outgoingRingtoneRunnable);
|
||||
mainHandler.postDelayed(outgoingRingtoneRunnable, 1500);
|
||||
}
|
||||
|
||||
if (pendingMediaCapture) {
|
||||
pendingMediaCapture = false;
|
||||
callService.startMediaCapture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +387,10 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getMediaCaptureReady() {
|
||||
return mediaCaptureReady;
|
||||
}
|
||||
|
||||
// State Update Methods (CallService)
|
||||
|
||||
public void updateConnectionState(PeerConnection.PeerConnectionState state) {
|
||||
@@ -375,6 +430,11 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
isFrontCamera.postValue(front);
|
||||
}
|
||||
|
||||
public void updateMediaCaptureReady(boolean ready) {
|
||||
Log.d(TAG, "updateMediaCaptureReady: " + ready);
|
||||
mediaCaptureReady.postValue(ready);
|
||||
}
|
||||
|
||||
public void reportError(String error) {
|
||||
Log.e(TAG, "reportError: " + error);
|
||||
errorMessage.postValue(error);
|
||||
@@ -387,7 +447,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
if (callService != null) {
|
||||
callService.startMediaCapture();
|
||||
} else {
|
||||
Log.w(TAG, "Cannot start media capture, service not ready");
|
||||
Log.d(TAG, "Service not ready, deferring media capture");
|
||||
pendingMediaCapture = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +460,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
wasAnsweredLocally = true;
|
||||
|
||||
if (callService != null) {
|
||||
callService.stopRingtone();
|
||||
}
|
||||
@@ -562,28 +625,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
notifyBackendCallEnded();
|
||||
|
||||
// Disconnect with CallControlScope
|
||||
CallControlScope scope = activeCallControlScope;
|
||||
if (scope != null) {
|
||||
scope.disconnect(
|
||||
new DisconnectCause(DisconnectCause.REJECTED),
|
||||
new Continuation<CallControlResult>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public CoroutineContext getContext() {
|
||||
return EmptyCoroutineContext.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeWith(@NonNull Object result) {
|
||||
if (result instanceof CallControlResult) {
|
||||
Log.d(TAG, "Decline succeeded with CallControlScope");
|
||||
} else if (result instanceof kotlin.Result.Failure) {
|
||||
Log.e(TAG, "Decline failed", ((kotlin.Result.Failure) result).exception);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
disconnectTelecom(new DisconnectCause(DisconnectCause.REJECTED));
|
||||
|
||||
// End call on service
|
||||
if (callService != null) {
|
||||
@@ -604,28 +646,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
notifyBackendCallEnded();
|
||||
|
||||
// Disconnect with CallControlScope
|
||||
CallControlScope scope = activeCallControlScope;
|
||||
if (scope != null) {
|
||||
scope.disconnect(
|
||||
new DisconnectCause(DisconnectCause.LOCAL),
|
||||
new Continuation<CallControlResult>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public CoroutineContext getContext() {
|
||||
return EmptyCoroutineContext.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeWith(@NonNull Object result) {
|
||||
if (result instanceof CallControlResult) {
|
||||
Log.d(TAG, "Hang up succeeded with CallControlScope");
|
||||
} else if (result instanceof kotlin.Result.Failure) {
|
||||
Log.e(TAG, "Hang up failed", ((kotlin.Result.Failure) result).exception);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
disconnectTelecom(new DisconnectCause(DisconnectCause.LOCAL));
|
||||
|
||||
// End call on service
|
||||
if (callService != null) {
|
||||
@@ -651,11 +672,17 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
public synchronized void setVideoEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setVideoEnabled: " + enabled);
|
||||
|
||||
if (callService != null) {
|
||||
boolean success = callService.setVideoEnabled(enabled);
|
||||
if (!success && enabled) {
|
||||
enabled = false;
|
||||
reportError("Camera unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
localVideoEnabled.postValue(enabled);
|
||||
|
||||
if (callService != null) {
|
||||
callService.setVideoEnabled(enabled);
|
||||
|
||||
callService.sendMutedState(Boolean.TRUE.equals(localAudioEnabled.getValue()), enabled);
|
||||
}
|
||||
}
|
||||
@@ -865,7 +892,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
// This event is problematic because it can trigger in both directions,
|
||||
// in addition to multiple other scenarios which cannot easily be distinguished
|
||||
// May cause problems in edge cases
|
||||
onCallEnded(accId, callId);
|
||||
onCallEnded(accId, callId, startsWithVideo);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -875,32 +902,15 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
int accId, int callId, String offerSdp, boolean startsWithVideo) {
|
||||
Log.d(TAG, "onIncomingCall: accId=" + accId + ", callId=" + callId);
|
||||
|
||||
if (hasActiveCall()) {
|
||||
Log.w(TAG, "Already have an active call, ignoring incoming call");
|
||||
return;
|
||||
}
|
||||
Pair<DcChat, String> result = setupIncomingCallState(accId, callId, offerSdp, startsWithVideo);
|
||||
if (result == null) return;
|
||||
|
||||
resetLiveDataForNewCall();
|
||||
|
||||
this.activeAccId = accId;
|
||||
this.activeCallId = callId;
|
||||
this.isIncomingCall = true;
|
||||
this.startsWithVideo = startsWithVideo;
|
||||
this.pendingOfferSdp = offerSdp;
|
||||
|
||||
// Get caller info
|
||||
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accId);
|
||||
int chatId = dcContext.getMsg(callId).getChatId();
|
||||
this.activeChatId = chatId;
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
String callerName = getNameFromChat(dcChat);
|
||||
DcChat dcChat = result.first;
|
||||
String callerName = result.second;
|
||||
Icon callerIcon = getIconFromChat(this.appContext, dcChat);
|
||||
|
||||
displayName.postValue(callerName);
|
||||
displayIcon.postValue(callerIcon);
|
||||
|
||||
this.preferredStartingEndpoint = getPreferredStartingEndpoint(startsWithVideo);
|
||||
|
||||
// Add to CallsManager
|
||||
CallAttributesCompat callAttributes = createCallAttributes(callerName, callId, true);
|
||||
addCallToTelecom(callAttributes, callerName, callerIcon);
|
||||
@@ -911,6 +921,37 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
startAndBindService();
|
||||
}
|
||||
|
||||
public synchronized void handleIncomingCallFromConversation(
|
||||
int accId, int callId, String offerSdp, boolean hasVideo) {
|
||||
Log.d(TAG, "handleIncomingCallFromConversation: accId=" + accId + ", callId=" + callId);
|
||||
|
||||
if (offerSdp == null || offerSdp.isEmpty()) {
|
||||
Log.e(TAG, "Cannot start incoming call: no SDP offer");
|
||||
return;
|
||||
}
|
||||
|
||||
Pair<DcChat, String> result = setupIncomingCallState(accId, callId, offerSdp, hasVideo);
|
||||
if (result == null) return;
|
||||
|
||||
DcChat dcChat = result.first;
|
||||
String callerName = result.second;
|
||||
|
||||
new Thread(
|
||||
() -> {
|
||||
Icon callerIcon = getIconFromChat(this.appContext, dcChat);
|
||||
displayIcon.postValue(callerIcon);
|
||||
})
|
||||
.start();
|
||||
|
||||
// Add to CallsManager
|
||||
CallAttributesCompat callAttributes = createCallAttributes(callerName, callId, true);
|
||||
addCallToTelecom(callAttributes, callerName, null);
|
||||
|
||||
startAndBindService();
|
||||
|
||||
launchCallActivity();
|
||||
}
|
||||
|
||||
private synchronized void onIncomingCallAccepted(int callId, boolean fromThisDevice) {
|
||||
Log.d(TAG, "onIncomingCallAccepted: callId=" + callId + ", fromThisDevice=" + fromThisDevice);
|
||||
|
||||
@@ -952,24 +993,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
answeredElsewhere.postValue(true);
|
||||
|
||||
// Disconnect from Telecom CallControlScope
|
||||
CallControlScope scope = activeCallControlScope;
|
||||
if (scope != null) {
|
||||
scope.disconnect(
|
||||
new DisconnectCause(DisconnectCause.REMOTE),
|
||||
new Continuation<CallControlResult>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public CoroutineContext getContext() {
|
||||
return EmptyCoroutineContext.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeWith(@NonNull Object result) {
|
||||
Log.d(TAG, "Disconnect (answered elsewhere) completed");
|
||||
}
|
||||
});
|
||||
}
|
||||
disconnectTelecom(new DisconnectCause(DisconnectCause.REMOTE));
|
||||
|
||||
if (callService != null) {
|
||||
callService.endCall();
|
||||
@@ -1019,7 +1043,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
showOrUpdateOngoingNotification(appContext.getString(R.string.call_with, calleeName));
|
||||
}
|
||||
|
||||
private synchronized void onCallEnded(int accId, int callId) {
|
||||
private synchronized void onCallEnded(int accId, int callId, boolean startsWithVideo) {
|
||||
Log.d(TAG, "onCallEnded: accId=" + accId + ", callId=" + callId);
|
||||
|
||||
if (!hasActiveCall()) {
|
||||
@@ -1047,30 +1071,16 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
callService.stopRingtone();
|
||||
}
|
||||
|
||||
// Disconnect from CallControlScope
|
||||
if (activeCallControlScope != null) {
|
||||
activeCallControlScope.disconnect(
|
||||
// We actually don't know if this is incoming or outgoing
|
||||
// But we have to provide one of LOCAL, REMOTE, MISSED, REJECTED
|
||||
new DisconnectCause(DisconnectCause.REMOTE),
|
||||
new Continuation<CallControlResult>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public CoroutineContext getContext() {
|
||||
return EmptyCoroutineContext.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeWith(@NonNull Object result) {
|
||||
Log.d(TAG, "Disconnect completed");
|
||||
}
|
||||
});
|
||||
}
|
||||
disconnectTelecom(new DisconnectCause(DisconnectCause.REMOTE));
|
||||
|
||||
if (callService != null) {
|
||||
callService.endCall();
|
||||
}
|
||||
|
||||
if (isIncomingCall && !wasAnsweredLocally) {
|
||||
showMissedCallNotification(activeAccId, activeChatId, startsWithVideo);
|
||||
}
|
||||
|
||||
// Clear active states
|
||||
cleanupCall(accId, callId);
|
||||
}
|
||||
@@ -1089,6 +1099,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
notifyBackendCallEnded();
|
||||
|
||||
DisconnectCause cause;
|
||||
if (state == PeerConnection.PeerConnectionState.FAILED) {
|
||||
cause = new DisconnectCause(DisconnectCause.REMOTE, "PeerConnection failed");
|
||||
} else {
|
||||
cause = new DisconnectCause(DisconnectCause.LOCAL, "PeerConnection closed");
|
||||
}
|
||||
disconnectTelecom(cause);
|
||||
|
||||
if (callService != null) {
|
||||
callService.endCall();
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (hasActiveCall()) {
|
||||
cleanupCall(activeAccId, activeCallId);
|
||||
@@ -1129,6 +1151,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
this.pendingOfferSdp = null;
|
||||
this.hasNotifiedBackend = false;
|
||||
this.hasAutoSelectedEarpiece = false;
|
||||
this.pendingMediaCapture = false;
|
||||
this.wasAnsweredLocally = false;
|
||||
|
||||
mainHandler.removeCallbacks(outgoingRingtoneRunnable);
|
||||
|
||||
@@ -1194,6 +1218,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
private void resetLiveDataForNewCall() {
|
||||
connectionState.postValue(PeerConnection.PeerConnectionState.NEW);
|
||||
answeredElsewhere.postValue(false); // clearLiveData() must not reset answeredElsewhere
|
||||
mediaCaptureReady.postValue(false);
|
||||
clearLiveData();
|
||||
}
|
||||
|
||||
@@ -1209,6 +1234,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
outgoingCallPlaced.postValue(false);
|
||||
currentAudioEndpoint.postValue(null);
|
||||
availableAudioEndpoints.postValue(null);
|
||||
mediaCaptureReady.postValue(false);
|
||||
}
|
||||
|
||||
public synchronized void initiateOutgoingCall(int accId, int chatId, boolean startsWithVideo) {
|
||||
@@ -1219,16 +1245,6 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check microphone permission
|
||||
if (!hasMicrophonePermission()) {
|
||||
Log.e(TAG, "Microphone permission not granted");
|
||||
|
||||
Intent intent = new Intent(appContext, CallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
appContext.startActivity(intent);
|
||||
return;
|
||||
}
|
||||
|
||||
resetLiveDataForNewCall();
|
||||
|
||||
this.activeCallId = -1; // Placeholder call ID for Intent
|
||||
@@ -1254,7 +1270,9 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
|
||||
this.preferredStartingEndpoint = getPreferredStartingEndpoint(startsWithVideo);
|
||||
|
||||
startAndBindService();
|
||||
if (hasMicrophonePermission()) {
|
||||
startAndBindService();
|
||||
}
|
||||
|
||||
launchCallActivity();
|
||||
}
|
||||
@@ -1286,6 +1304,43 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
addCallToTelecom(callAttributes, calleeName, calleeIcon);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Pair<DcChat, String> setupIncomingCallState(
|
||||
int accId, int callId, String offerSdp, boolean startsWithVideo) {
|
||||
if (hasActiveCall()) {
|
||||
Log.w(TAG, "Already have an active call, ignoring incoming call");
|
||||
return null;
|
||||
}
|
||||
|
||||
resetLiveDataForNewCall();
|
||||
|
||||
this.activeAccId = accId;
|
||||
this.activeCallId = callId;
|
||||
this.isIncomingCall = true;
|
||||
this.startsWithVideo = startsWithVideo;
|
||||
this.pendingOfferSdp = offerSdp;
|
||||
|
||||
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accId);
|
||||
int chatId = dcContext.getMsg(callId).getChatId();
|
||||
this.activeChatId = chatId;
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
String callerName = getNameFromChat(dcChat);
|
||||
|
||||
displayName.postValue(callerName);
|
||||
|
||||
this.preferredStartingEndpoint = getPreferredStartingEndpoint(startsWithVideo);
|
||||
|
||||
return new Pair<>(dcChat, callerName);
|
||||
}
|
||||
|
||||
public synchronized void ensureServiceStarted() {
|
||||
if (isServiceBound || !hasActiveCall()) {
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "Starting service after permission grant");
|
||||
startAndBindService();
|
||||
}
|
||||
|
||||
private void addCallToTelecom(
|
||||
CallAttributesCompat callAttributes, String displayName, Icon icon) {
|
||||
try {
|
||||
@@ -1388,19 +1443,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
PendingIntent answerPendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this.appContext,
|
||||
0,
|
||||
PI_ANSWER,
|
||||
answerIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
// Decline intent
|
||||
Intent declineIntent = new Intent(this.appContext, CallActivity.class);
|
||||
Intent declineIntent = new Intent(this.appContext, CallActionReceiver.class);
|
||||
declineIntent.setAction(CallActivity.ACTION_DECLINE_CALL);
|
||||
declineIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
PendingIntent declinePendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
PendingIntent.getBroadcast(
|
||||
this.appContext,
|
||||
1,
|
||||
PI_DECLINE,
|
||||
declineIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
@@ -1411,7 +1465,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
PendingIntent fullScreenPendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this.appContext,
|
||||
2,
|
||||
PI_FULLSCREEN,
|
||||
fullScreenIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
@@ -1462,26 +1516,106 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
|
||||
notificationManager.notify(NOTIFICATION_ID_CALL, builder.build());
|
||||
}
|
||||
|
||||
private void showMissedCallNotification(int accId, int chatId, boolean wasVideoCall) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (!hasNotificationPermission()) {
|
||||
Log.w(TAG, "Cannot show missed call notification: no permission");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accId);
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
String callerName = CallUtil.getNameFromChat(dcChat);
|
||||
|
||||
Intent contentAction = new Intent(appContext, ConversationActivity.class);
|
||||
contentAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
|
||||
contentAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
|
||||
contentAction.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
PendingIntent contentIntent =
|
||||
PendingIntent.getActivity(
|
||||
appContext,
|
||||
PI_MISSED_CONTENT,
|
||||
contentAction,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
Intent callBackAction = new Intent(appContext, CallActionReceiver.class);
|
||||
callBackAction.setAction(CallActivity.ACTION_CALL_BACK);
|
||||
callBackAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
|
||||
callBackAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
|
||||
callBackAction.putExtra(CallActivity.EXTRA_STARTS_WITH_VIDEO, wasVideoCall);
|
||||
|
||||
PendingIntent callBackIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
appContext,
|
||||
PI_MISSED_CALLBACK,
|
||||
callBackAction,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
Intent messageAction = new Intent(appContext, CallActionReceiver.class);
|
||||
messageAction.setAction(CallActivity.ACTION_MESSAGE);
|
||||
messageAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
|
||||
messageAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
|
||||
|
||||
PendingIntent messageIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
appContext,
|
||||
PI_MISSED_MESSAGE,
|
||||
messageAction,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
String contentText = appContext.getString(R.string.missed_call);
|
||||
|
||||
Notification.Builder builder =
|
||||
new Notification.Builder(appContext, CHANNEL_ID_MISSED)
|
||||
.setSmallIcon(R.drawable.icon_notification)
|
||||
.setContentTitle(callerName)
|
||||
.setContentText(contentText)
|
||||
.setContentIntent(contentIntent)
|
||||
.setAutoCancel(true)
|
||||
.addAction(
|
||||
new Notification.Action.Builder(
|
||||
null, appContext.getString(R.string.call_back), callBackIntent)
|
||||
.build())
|
||||
.addAction(
|
||||
new Notification.Action.Builder(
|
||||
null, appContext.getString(R.string.chat_input_placeholder), messageIntent)
|
||||
.build());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setCategory(Notification.CATEGORY_MISSED_CALL);
|
||||
} else {
|
||||
builder.setCategory(Notification.CATEGORY_CALL);
|
||||
}
|
||||
|
||||
Icon icon = displayIcon.getValue();
|
||||
if (icon != null) {
|
||||
builder.setLargeIcon(icon);
|
||||
}
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID_MISSED_CALL, builder.build());
|
||||
}
|
||||
|
||||
private Notification buildOngoingCallNotification(
|
||||
String statusText, String displayName, Icon icon) {
|
||||
Intent activityIntent = new Intent(this.appContext, CallActivity.class);
|
||||
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
Intent hangupIntent = new Intent(this.appContext, CallActivity.class);
|
||||
Intent hangupIntent = new Intent(this.appContext, CallActionReceiver.class);
|
||||
hangupIntent.setAction(CallActivity.ACTION_HANGUP_CALL);
|
||||
hangupIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
PendingIntent hangupPendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
PendingIntent.getBroadcast(
|
||||
this.appContext,
|
||||
3,
|
||||
PI_HANGUP,
|
||||
hangupIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
PendingIntent contentIntent =
|
||||
PendingIntent.getActivity(
|
||||
this.appContext,
|
||||
4,
|
||||
PI_ONGOING_CONTENT,
|
||||
activityIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
|
||||
@@ -126,13 +126,13 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start camera/microphone capture
|
||||
* Start media capture
|
||||
*
|
||||
* <p>Must be called when app is in foreground. Called by coordinator when ViewModel/Activity is
|
||||
* ready.
|
||||
*/
|
||||
public void startMediaCapture() {
|
||||
Log.d(TAG, "startMediaCapture (Camera/Microphone)");
|
||||
Log.d(TAG, "startMediaCapture");
|
||||
|
||||
if (webRTCClient != null && webRTCClient.hasLocalMediaStream()) {
|
||||
Log.w(TAG, "Media already initialized, skipping");
|
||||
@@ -153,7 +153,7 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
|
||||
boolean startsWithVideo = callCoordinator.isStartsWithVideo();
|
||||
|
||||
Log.d(TAG, "Creating media stream with video: " + startsWithVideo);
|
||||
Log.d(TAG, "Creating media stream");
|
||||
|
||||
mediaStreamManager.createMediaStream(
|
||||
new MediaStreamManager.Callback() {
|
||||
@@ -163,31 +163,22 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
|
||||
webRTCClient.setLocalMediaStream(stream);
|
||||
|
||||
callCoordinator.updateFrontCamera(mediaStreamManager.isFrontCamera());
|
||||
|
||||
callCoordinator.setVideoEnabled(startsWithVideo);
|
||||
|
||||
if (!stream.videoTracks.isEmpty()) {
|
||||
VideoTrack localTrack = stream.videoTracks.get(0);
|
||||
callCoordinator.updateLocalVideoTrack(localTrack);
|
||||
} else {
|
||||
Log.w(TAG, "No video track in stream, call will be audio-only");
|
||||
if (startsWithVideo) {
|
||||
callCoordinator.reportError("Camera unavailable, using audio only");
|
||||
}
|
||||
callCoordinator.setVideoEnabled(false);
|
||||
}
|
||||
|
||||
callCoordinator.setVideoEnabled(startsWithVideo);
|
||||
|
||||
callCoordinator.updateMediaCaptureReady(true);
|
||||
|
||||
Log.d(TAG, "Media capture complete, ready for call");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "Failed to setup media: " + error);
|
||||
if (startsWithVideo) {
|
||||
callCoordinator.reportError("Camera/microphone error: " + error);
|
||||
}
|
||||
callCoordinator.setVideoEnabled(false);
|
||||
callCoordinator.reportError("Camera/microphone error: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -395,12 +386,30 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
}
|
||||
}
|
||||
|
||||
public void setVideoEnabled(boolean enabled) {
|
||||
public boolean setVideoEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setVideoEnabled: " + enabled);
|
||||
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.setVideoEnabled(enabled);
|
||||
if (enabled) {
|
||||
if (mediaStreamManager != null) {
|
||||
boolean captureReady = mediaStreamManager.startVideoCapture();
|
||||
if (!captureReady) {
|
||||
Log.w(TAG, "Failed to start video capture");
|
||||
return false;
|
||||
}
|
||||
callCoordinator.updateFrontCamera(mediaStreamManager.isFrontCamera());
|
||||
}
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.setVideoEnabled(true);
|
||||
}
|
||||
} else {
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.setVideoEnabled(false);
|
||||
}
|
||||
if (mediaStreamManager != null) {
|
||||
mediaStreamManager.stopVideoCapture();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void sendMutedState(boolean audioEnabled, boolean videoEnabled) {
|
||||
|
||||
@@ -5,13 +5,18 @@ import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.telecom.CallEndpointCompat;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -52,8 +57,22 @@ public class CallUtil {
|
||||
return;
|
||||
}
|
||||
|
||||
int accId = DcHelper.getContext(context).getAccountId();
|
||||
coordinator.initiateOutgoingCall(accId, chatId, startsWithVideo);
|
||||
Runnable proceedWithCall =
|
||||
() -> {
|
||||
int accId = DcHelper.getContext(context).getAccountId();
|
||||
coordinator.initiateOutgoingCall(accId, chatId, startsWithVideo);
|
||||
};
|
||||
|
||||
if (!isNetworkAvailable(context)) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.call_requires_connection))
|
||||
.setPositiveButton(R.string.perm_continue, (dialog, which) -> proceedWithCall.run())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
proceedWithCall.run();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -138,4 +157,24 @@ public class CallUtil {
|
||||
}
|
||||
return iconRes;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static boolean isNetworkAvailable(Context context) {
|
||||
ConnectivityManager manager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (manager == null) return true;
|
||||
|
||||
boolean networkAvailable = false;
|
||||
Network network = manager.getActiveNetwork();
|
||||
if (network != null) {
|
||||
NetworkCapabilities caps = manager.getNetworkCapabilities(network);
|
||||
networkAvailable =
|
||||
caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
||||
}
|
||||
|
||||
boolean serverConnected =
|
||||
DcHelper.getContext(context).getConnectivity() >= DcContext.DC_CONNECTIVITY_WORKING;
|
||||
|
||||
return networkAvailable || serverConnected;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ public class CallViewModel extends AndroidViewModel {
|
||||
private final MediatorLiveData<CallState> callState;
|
||||
|
||||
// Observer References for one-time observe
|
||||
private Observer<VideoTrack> answerCallObserver;
|
||||
private Observer<VideoTrack> startOutgoingCallObserver;
|
||||
private Observer<Boolean> answerCallObserver;
|
||||
private Observer<Boolean> startOutgoingCallObserver;
|
||||
|
||||
private final AtomicBoolean hasCallEnded = new AtomicBoolean(false);
|
||||
|
||||
@@ -211,25 +211,24 @@ public class CallViewModel extends AndroidViewModel {
|
||||
callCoordinator.startMediaCapture();
|
||||
|
||||
// Create one-time observer
|
||||
LiveData<VideoTrack> localTrack = callCoordinator.getLocalVideoTrack();
|
||||
LiveData<Boolean> mediaReady = callCoordinator.getMediaCaptureReady();
|
||||
|
||||
answerCallObserver =
|
||||
new Observer<VideoTrack>() {
|
||||
new Observer<Boolean>() {
|
||||
@Override
|
||||
public void onChanged(VideoTrack videoTrack) {
|
||||
if (videoTrack != null) {
|
||||
// Media is ready, remove observer
|
||||
localTrack.removeObserver(this);
|
||||
public void onChanged(Boolean ready) {
|
||||
if (Boolean.TRUE.equals(ready)) {
|
||||
mediaReady.removeObserver(this);
|
||||
answerCallObserver = null;
|
||||
|
||||
Log.d(TAG, "Local video ready, answering call (WebRTC)");
|
||||
Log.d(TAG, "Media capture ready, answering call (WebRTC)");
|
||||
|
||||
callCoordinator.answerWebRTC();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
localTrack.observeForever(answerCallObserver);
|
||||
mediaReady.observeForever(answerCallObserver);
|
||||
}
|
||||
|
||||
/** Start outgoing call with media capture Called by Activity for outgoing calls */
|
||||
@@ -244,30 +243,28 @@ public class CallViewModel extends AndroidViewModel {
|
||||
callCoordinator.startMediaCapture();
|
||||
|
||||
// Create one-time observer
|
||||
LiveData<VideoTrack> localTrack = callCoordinator.getLocalVideoTrack();
|
||||
VideoTrack currentValue = localTrack.getValue();
|
||||
LiveData<Boolean> mediaReady = callCoordinator.getMediaCaptureReady();
|
||||
|
||||
if (currentValue != null) {
|
||||
if (Boolean.TRUE.equals(mediaReady.getValue())) {
|
||||
Log.d(TAG, "Media already ready, starting call immediately");
|
||||
callCoordinator.startOutgoingCall();
|
||||
} else {
|
||||
startOutgoingCallObserver =
|
||||
new Observer<VideoTrack>() {
|
||||
new Observer<Boolean>() {
|
||||
@Override
|
||||
public void onChanged(VideoTrack videoTrack) {
|
||||
if (videoTrack != null) {
|
||||
// Media is ready, remove observer
|
||||
localTrack.removeObserver(this);
|
||||
public void onChanged(Boolean ready) {
|
||||
if (Boolean.TRUE.equals(ready)) {
|
||||
mediaReady.removeObserver(this);
|
||||
startOutgoingCallObserver = null;
|
||||
|
||||
Log.d(TAG, "Local video ready, starting outgoing call");
|
||||
Log.d(TAG, "Media capture ready, starting outgoing call");
|
||||
|
||||
callCoordinator.startOutgoingCall();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
localTrack.observeForever(startOutgoingCallObserver);
|
||||
mediaReady.observeForever(startOutgoingCallObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,12 +457,12 @@ public class CallViewModel extends AndroidViewModel {
|
||||
Log.d(TAG, "CallViewModel cleared");
|
||||
|
||||
if (answerCallObserver != null) {
|
||||
callCoordinator.getLocalVideoTrack().removeObserver(answerCallObserver);
|
||||
callCoordinator.getMediaCaptureReady().removeObserver(answerCallObserver);
|
||||
answerCallObserver = null;
|
||||
}
|
||||
|
||||
if (startOutgoingCallObserver != null) {
|
||||
callCoordinator.getLocalVideoTrack().removeObserver(startOutgoingCallObserver);
|
||||
callCoordinator.getMediaCaptureReady().removeObserver(startOutgoingCallObserver);
|
||||
startOutgoingCallObserver = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import java.util.List;
|
||||
import org.thoughtcrime.securesms.EglUtils;
|
||||
import org.webrtc.AudioSource;
|
||||
import org.webrtc.AudioTrack;
|
||||
import org.webrtc.Camera2Enumerator;
|
||||
import org.webrtc.CameraEnumerationAndroid;
|
||||
import org.webrtc.CameraVideoCapturer;
|
||||
import org.webrtc.MediaConstraints;
|
||||
import org.webrtc.MediaStream;
|
||||
@@ -22,6 +24,7 @@ import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoSource;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
public class MediaStreamManager {
|
||||
|
||||
private static final String TAG = "MediaStreamManager";
|
||||
@@ -29,6 +32,10 @@ public class MediaStreamManager {
|
||||
private static final String AUDIO_TRACK_ID = "audio_track";
|
||||
private static final String VIDEO_TRACK_ID = "video_track";
|
||||
|
||||
private static final int TARGET_WIDTH = 1280;
|
||||
private static final int TARGET_HEIGHT = 720;
|
||||
private static final int TARGET_FPS = 30;
|
||||
|
||||
private final Context context;
|
||||
private final PeerConnectionFactory peerConnectionFactory;
|
||||
|
||||
@@ -37,6 +44,10 @@ public class MediaStreamManager {
|
||||
private AudioSource audioSource;
|
||||
private SurfaceTextureHelper surfaceTextureHelper;
|
||||
private volatile boolean isFrontCamera = true;
|
||||
private volatile boolean isCapturing = false;
|
||||
private volatile String currentDeviceName;
|
||||
private volatile int currentCaptureWidth;
|
||||
private volatile int currentCaptureHeight;
|
||||
|
||||
public interface Callback {
|
||||
void onMediaStreamReady(MediaStream stream);
|
||||
@@ -56,9 +67,8 @@ public class MediaStreamManager {
|
||||
this.peerConnectionFactory = peerConnectionFactory;
|
||||
}
|
||||
|
||||
/** Create media stream with audio and optionally video */
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void createMediaStream(Callback callback) {
|
||||
/** Create a media stream with an audio track and a video track. */
|
||||
public synchronized void createMediaStream(Callback callback) {
|
||||
try {
|
||||
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream(STREAM_ID);
|
||||
|
||||
@@ -68,24 +78,11 @@ public class MediaStreamManager {
|
||||
AudioTrack audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
|
||||
mediaStream.addTrack(audioTrack);
|
||||
|
||||
// Create video track
|
||||
videoCapturer = createVideoCapturer();
|
||||
if (videoCapturer == null) {
|
||||
callback.onError("No camera available");
|
||||
callback.onMediaStreamReady(mediaStream);
|
||||
return;
|
||||
}
|
||||
|
||||
videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
|
||||
// Create video source and track
|
||||
videoSource = peerConnectionFactory.createVideoSource(false);
|
||||
VideoTrack videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
|
||||
mediaStream.addTrack(videoTrack);
|
||||
|
||||
// Start capturing
|
||||
surfaceTextureHelper =
|
||||
SurfaceTextureHelper.create("CaptureThread", EglUtils.getEglBase().getEglBaseContext());
|
||||
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
|
||||
videoCapturer.startCapture(1280, 720, 30);
|
||||
|
||||
callback.onMediaStreamReady(mediaStream);
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -94,6 +91,129 @@ public class MediaStreamManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the camera and start sending frames to VideoSource.
|
||||
*
|
||||
* @return true if the camera is capturing, false if it could not be started
|
||||
*/
|
||||
public synchronized boolean startVideoCapture() {
|
||||
if (isCapturing) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (videoSource == null) {
|
||||
Log.e(TAG, "VideoSource not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (videoCapturer == null) {
|
||||
videoCapturer = createVideoCapturer();
|
||||
if (videoCapturer == null) {
|
||||
Log.w(TAG, "Cannot start video capture: no camera available");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (surfaceTextureHelper == null) {
|
||||
surfaceTextureHelper =
|
||||
SurfaceTextureHelper.create("CaptureThread", EglUtils.getEglBase().getEglBaseContext());
|
||||
}
|
||||
|
||||
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
|
||||
}
|
||||
|
||||
int[] captureFormat = selectCaptureFormat(currentDeviceName);
|
||||
currentCaptureWidth = captureFormat[0];
|
||||
currentCaptureHeight = captureFormat[1];
|
||||
|
||||
videoCapturer.startCapture(currentCaptureWidth, currentCaptureHeight, TARGET_FPS);
|
||||
videoSource.adaptOutputFormat(TARGET_WIDTH, TARGET_HEIGHT, TARGET_FPS);
|
||||
isCapturing = true;
|
||||
Log.d(
|
||||
TAG,
|
||||
"Video capture started at "
|
||||
+ currentCaptureWidth
|
||||
+ "x"
|
||||
+ currentCaptureHeight
|
||||
+ ", adapted to "
|
||||
+ TARGET_WIDTH
|
||||
+ "x"
|
||||
+ TARGET_HEIGHT);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Stop the camera. The capturer is kept alive. */
|
||||
public synchronized void stopVideoCapture() {
|
||||
if (!isCapturing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoCapturer != null) {
|
||||
try {
|
||||
videoCapturer.stopCapture();
|
||||
Log.d(TAG, "Video capture stopped");
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted while stopping capture", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
isCapturing = false;
|
||||
}
|
||||
|
||||
private int[] selectCaptureFormat(@Nullable String deviceName) {
|
||||
if (deviceName == null) {
|
||||
Log.w(TAG, "Device name is null, using target dimensions");
|
||||
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
|
||||
}
|
||||
|
||||
Camera2Enumerator enumerator = new Camera2Enumerator(context);
|
||||
List<CameraEnumerationAndroid.CaptureFormat> formats =
|
||||
enumerator.getSupportedFormats(deviceName);
|
||||
|
||||
if (formats == null || formats.isEmpty()) {
|
||||
Log.w(TAG, "No supported formats for " + deviceName);
|
||||
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
|
||||
}
|
||||
|
||||
CameraEnumerationAndroid.CaptureFormat best = null;
|
||||
int bestPixels = Integer.MAX_VALUE;
|
||||
|
||||
for (CameraEnumerationAndroid.CaptureFormat f : formats) {
|
||||
if (f.width >= TARGET_WIDTH && f.height >= TARGET_HEIGHT) {
|
||||
int pixels = f.width * f.height;
|
||||
if (pixels < bestPixels) {
|
||||
bestPixels = pixels;
|
||||
best = f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best != null) {
|
||||
Log.d(
|
||||
TAG, "Selected capture format: " + best.width + "x" + best.height + " for " + deviceName);
|
||||
return new int[] {best.width, best.height};
|
||||
}
|
||||
|
||||
CameraEnumerationAndroid.CaptureFormat largest = null;
|
||||
int largestPixels = 0;
|
||||
for (CameraEnumerationAndroid.CaptureFormat f : formats) {
|
||||
int pixels = f.width * f.height;
|
||||
if (pixels > largestPixels) {
|
||||
largestPixels = pixels;
|
||||
largest = f;
|
||||
}
|
||||
}
|
||||
|
||||
if (largest != null) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Using largest format " + largest.width + "x" + largest.height + " for " + deviceName);
|
||||
return new int[] {largest.width, largest.height};
|
||||
}
|
||||
|
||||
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private VideoCapturer createVideoCapturer() {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
|
||||
@@ -111,6 +231,7 @@ public class MediaStreamManager {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
isFrontCamera = true;
|
||||
currentDeviceName = deviceName;
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
@@ -121,6 +242,7 @@ public class MediaStreamManager {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
isFrontCamera = enumerator.isFrontFacing(deviceName);
|
||||
currentDeviceName = deviceName;
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
@@ -129,6 +251,14 @@ public class MediaStreamManager {
|
||||
}
|
||||
|
||||
public void switchCamera(@Nullable CameraSwitchCallback callback) {
|
||||
if (!isCapturing) {
|
||||
Log.w(TAG, "Cannot switch camera while not capturing");
|
||||
if (callback != null) {
|
||||
callback.onError("Camera not active");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(videoCapturer instanceof CameraVideoCapturer)) {
|
||||
Log.e(TAG, "switchCamera called but videoCapturer is not a CameraVideoCapturer");
|
||||
return;
|
||||
@@ -169,6 +299,30 @@ public class MediaStreamManager {
|
||||
public void onCameraSwitchDone(boolean isFront) {
|
||||
Log.d(TAG, "switchCamera SUCCESS, isFront=" + isFront);
|
||||
isFrontCamera = isFront;
|
||||
currentDeviceName = finalTargetCameraName;
|
||||
|
||||
int[] newFormat = selectCaptureFormat(finalTargetCameraName);
|
||||
if (newFormat[0] != currentCaptureWidth || newFormat[1] != currentCaptureHeight) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Changing capture format: "
|
||||
+ currentCaptureWidth
|
||||
+ "x"
|
||||
+ currentCaptureHeight
|
||||
+ " to "
|
||||
+ newFormat[0]
|
||||
+ "x"
|
||||
+ newFormat[1]);
|
||||
currentCaptureWidth = newFormat[0];
|
||||
currentCaptureHeight = newFormat[1];
|
||||
cameraVideoCapturer.changeCaptureFormat(
|
||||
currentCaptureWidth, currentCaptureHeight, TARGET_FPS);
|
||||
}
|
||||
|
||||
if (videoSource != null) {
|
||||
videoSource.adaptOutputFormat(TARGET_WIDTH, TARGET_HEIGHT, TARGET_FPS);
|
||||
}
|
||||
|
||||
if (callback != null) callback.onCameraSwitch(isFront);
|
||||
}
|
||||
|
||||
@@ -186,10 +340,12 @@ public class MediaStreamManager {
|
||||
}
|
||||
|
||||
/** Cleanup resources */
|
||||
public void dispose() {
|
||||
public synchronized void dispose() {
|
||||
if (videoCapturer != null) {
|
||||
try {
|
||||
videoCapturer.stopCapture();
|
||||
if (isCapturing) {
|
||||
videoCapturer.stopCapture();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Error stopping capture", e);
|
||||
}
|
||||
@@ -197,6 +353,8 @@ public class MediaStreamManager {
|
||||
videoCapturer = null;
|
||||
}
|
||||
|
||||
isCapturing = false;
|
||||
|
||||
if (surfaceTextureHelper != null) {
|
||||
surfaceTextureHelper.dispose();
|
||||
surfaceTextureHelper = null;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import org.thoughtcrime.securesms.components.viewpager.HackyViewPager;
|
||||
|
||||
/** An implementation of {@link ViewPager} that disables swiping when the view is disabled. */
|
||||
public class ControllableViewPager extends HackyViewPager {
|
||||
|
||||
public ControllableViewPager(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ControllableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
return isEnabled() && super.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
return isEnabled() && super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
}
|
||||
@@ -360,6 +360,31 @@ public class InputPanel extends ConstraintLayout
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
return microphoneRecorderView.isRecording();
|
||||
}
|
||||
|
||||
public long getRecordingDuration() {
|
||||
return recordTime.getElapsedTime();
|
||||
}
|
||||
|
||||
public void resetRecordingUI() {
|
||||
microphoneRecorderView.resetState();
|
||||
recordLockCancel.setVisibility(View.GONE);
|
||||
recordTime.hide();
|
||||
slideToCancel.hide();
|
||||
|
||||
emojiToggle.setVisibility(View.VISIBLE);
|
||||
emojiToggle.setAlpha(1f);
|
||||
composeText.setVisibility(View.VISIBLE);
|
||||
composeText.setAlpha(1f);
|
||||
quickCameraToggle.setVisibility(View.VISIBLE);
|
||||
quickCameraToggle.setAlpha(1f);
|
||||
quickAudioToggle.setVisibility(View.VISIBLE);
|
||||
quickAudioToggle.setAlpha(1f);
|
||||
buttonToggle.setAlpha(1f);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onRecorderStarted();
|
||||
|
||||
@@ -452,10 +477,10 @@ public class InputPanel extends ConstraintLayout
|
||||
}
|
||||
|
||||
public long hide() {
|
||||
long elapsedtime = System.currentTimeMillis() - startTime.get();
|
||||
long elapsedTime = getElapsedTime();
|
||||
this.startTime.set(0);
|
||||
ViewUtil.fadeOut(this.recordTimeView, FADE_TIME, View.INVISIBLE);
|
||||
return elapsedtime;
|
||||
return elapsedTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -468,6 +493,11 @@ public class InputPanel extends ConstraintLayout
|
||||
}
|
||||
}
|
||||
|
||||
public long getElapsedTime() {
|
||||
long start = startTime.get();
|
||||
return start > 0 ? System.currentTimeMillis() - start : 0;
|
||||
}
|
||||
|
||||
private String formatElapsedTime(long ms) {
|
||||
return DateUtils.formatElapsedTime(TimeUnit.MILLISECONDS.toSeconds(ms))
|
||||
+ String.format(".%01d", ((ms / 100) % 10));
|
||||
|
||||
@@ -53,10 +53,11 @@ public class MediaView extends FrameLayout {
|
||||
@NonNull Window window,
|
||||
@NonNull Uri source,
|
||||
@Nullable String fileName,
|
||||
@NonNull String mediaType,
|
||||
@Nullable String mediaType,
|
||||
long size,
|
||||
boolean autoplay)
|
||||
throws IOException {
|
||||
mediaType = mediaType == null ? "null" : mediaType;
|
||||
if (mediaType.startsWith("image/")) {
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
if (videoView.resolved()) videoView.get().setVisibility(View.GONE);
|
||||
|
||||
@@ -287,4 +287,15 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
return state != State.NOT_RUNNING;
|
||||
}
|
||||
|
||||
public void resetState() {
|
||||
if (state != State.NOT_RUNNING) {
|
||||
state = State.NOT_RUNNING;
|
||||
hideUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -27,13 +27,11 @@ import java.util.concurrent.Executors;
|
||||
public class AudioPlaybackViewModel extends ViewModel {
|
||||
private static final String TAG = "AudioPlaybackViewModel";
|
||||
|
||||
private static final int NON_MESSAGE_AUDIO_MSG_ID =
|
||||
0; // Audios not attached to a message doesn't have message id.
|
||||
|
||||
private final MutableLiveData<AudioPlaybackState> playbackState;
|
||||
|
||||
private final MutableLiveData<Map<Integer, Long>> durations =
|
||||
new MutableLiveData<>(new HashMap<>());
|
||||
private final Map<Integer, Uri> durationUris = new HashMap<>();
|
||||
private final Set<Integer> extractionInProgress = new HashSet<>();
|
||||
private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2);
|
||||
|
||||
@@ -115,7 +113,17 @@ public class AudioPlaybackViewModel extends ViewModel {
|
||||
// Check cache
|
||||
Map<Integer, Long> currentDurations = durations.getValue();
|
||||
if (currentDurations != null && currentDurations.containsKey(msgId)) {
|
||||
return;
|
||||
Uri cachedUri = durationUris.get(msgId);
|
||||
if (audioUri.equals(cachedUri)) {
|
||||
return;
|
||||
}
|
||||
Map<Integer, Long> updated = new HashMap<>(currentDurations);
|
||||
updated.remove(msgId);
|
||||
durations.setValue(updated);
|
||||
durationUris.remove(msgId);
|
||||
synchronized (extractionInProgress) {
|
||||
extractionInProgress.remove(msgId);
|
||||
}
|
||||
}
|
||||
|
||||
// Check extracting
|
||||
@@ -136,6 +144,7 @@ public class AudioPlaybackViewModel extends ViewModel {
|
||||
Map<Integer, Long> updatedDurations = new HashMap<>(durations.getValue());
|
||||
updatedDurations.put(msgId, duration);
|
||||
durations.setValue(updatedDurations);
|
||||
durationUris.put(msgId, audioUri);
|
||||
});
|
||||
|
||||
synchronized (extractionInProgress) {
|
||||
@@ -193,10 +202,6 @@ public class AudioPlaybackViewModel extends ViewModel {
|
||||
return current != null && String.valueOf(msgId).equals(current.mediaId);
|
||||
}
|
||||
|
||||
public void stopNonMessageAudioPlayback() {
|
||||
stopByIds(NON_MESSAGE_AUDIO_MSG_ID);
|
||||
}
|
||||
|
||||
// A special method for deleting message, where we only use message Ids
|
||||
public void stopByIds(int... msgIds) {
|
||||
if (mediaController == null) return;
|
||||
@@ -259,7 +264,10 @@ public class AudioPlaybackViewModel extends ViewModel {
|
||||
updateCurrentState(false);
|
||||
} else if (player.getPlaybackState() == Player.STATE_ENDED
|
||||
&& !player.hasNextMediaItem()) {
|
||||
mediaController.setPlayWhenReady(false);
|
||||
mediaController.stop();
|
||||
mediaController.clearMediaItems();
|
||||
stopUpdateProgress();
|
||||
playbackState.setValue(AudioPlaybackState.idle());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,6 +394,7 @@ public class AudioPlaybackViewModel extends ViewModel {
|
||||
protected void onCleared() {
|
||||
stopUpdateProgress();
|
||||
extractionExecutor.shutdown();
|
||||
durationUris.clear();
|
||||
super.onCleared();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,10 @@ public class AudioView extends FrameLayout {
|
||||
|
||||
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
|
||||
|
||||
if (state != null && msgId == state.getMsgId()) {
|
||||
if (state != null
|
||||
&& msgId == state.getMsgId()
|
||||
&& (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING
|
||||
|| state.getStatus() == AudioPlaybackState.PlaybackStatus.PAUSED)) {
|
||||
// Same audio
|
||||
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
|
||||
viewModel.pause(msgId);
|
||||
@@ -194,14 +197,17 @@ public class AudioView extends FrameLayout {
|
||||
|
||||
seekBar.setEnabled(true);
|
||||
|
||||
this.progress = 0;
|
||||
this.duration = 0;
|
||||
|
||||
viewModel.ensureDurationLoaded(getContext(), msgId, audioUri);
|
||||
|
||||
// Get duration
|
||||
Map<Integer, Long> durations = viewModel.getDurations().getValue();
|
||||
if (durations != null && durations.containsKey(msgId)) {
|
||||
this.duration = Math.toIntExact(durations.get(msgId));
|
||||
updateTimestampsAndSeekBar();
|
||||
} else {
|
||||
viewModel.ensureDurationLoaded(getContext(), msgId, audioUri);
|
||||
}
|
||||
updateTimestampsAndSeekBar();
|
||||
|
||||
if (audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
|
||||
title.setVisibility(View.GONE);
|
||||
@@ -340,16 +346,18 @@ public class AudioView extends FrameLayout {
|
||||
private void onDurationsChanged(Map<Integer, Long> durations) {
|
||||
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
|
||||
|
||||
// When there is no playback happening, msgId can be -1
|
||||
if (state != null && msgId >= 0 && msgId == state.getMsgId()) {
|
||||
if (state != null
|
||||
&& msgId >= 0
|
||||
&& msgId == state.getMsgId()
|
||||
&& (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING
|
||||
|| state.getStatus() == AudioPlaybackState.PlaybackStatus.PAUSED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long duration = durations.get(msgId);
|
||||
if (duration != null && seekBar.getMax() <= 100) {
|
||||
if (duration != null) {
|
||||
this.duration = Math.toIntExact(duration);
|
||||
updateTimestampsAndSeekBar();
|
||||
seekBar.setMax(this.duration);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.viewpager;
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
public abstract class ExtendedOnPageChangedListener implements ViewPager.OnPageChangeListener {
|
||||
|
||||
private Integer currentPage = null;
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (currentPage != null && currentPage != position) onPageUnselected(currentPage);
|
||||
currentPage = position;
|
||||
}
|
||||
|
||||
public abstract void onPageUnselected(int position);
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.viewpager;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
/**
|
||||
* Hacky fix for http://code.google.com/p/android/issues/detail?id=18990
|
||||
*
|
||||
* <p>ScaleGestureDetector seems to mess up the touch events, which means that ViewGroups which make
|
||||
* use of onInterceptTouchEvent throw a lot of IllegalArgumentException: pointerIndex out of range.
|
||||
*
|
||||
* <p>There's not much I can do in my code for now, but we can mask the result by just catching the
|
||||
* problem and ignoring it.
|
||||
*
|
||||
* @author Chris Banes
|
||||
*/
|
||||
public class HackyViewPager extends ViewPager {
|
||||
|
||||
private static final String TAG = "HackyViewPager";
|
||||
|
||||
public HackyViewPager(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public HackyViewPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
try {
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
|
||||
if (query == null && addScanQRLink) {
|
||||
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_QR_INVITE);
|
||||
}
|
||||
if (addCreateContactLink && !dcContext.isChatmail()) {
|
||||
if (addCreateContactLink && dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
|
||||
additional_items =
|
||||
Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
|
||||
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_GROUP);
|
||||
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST);
|
||||
|
||||
if (!dcContext.isChatmail()) {
|
||||
if (dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
|
||||
additional_items =
|
||||
Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcLot;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
@@ -31,6 +35,7 @@ import org.thoughtcrime.securesms.LocalHelpActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShareActivity;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationCenter;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.qr.QrActivity;
|
||||
@@ -48,16 +53,14 @@ public class DcHelper {
|
||||
public static final String CONFIG_DISPLAY_NAME = "displayname";
|
||||
public static final String CONFIG_SELF_STATUS = "selfstatus";
|
||||
public static final String CONFIG_SELF_AVATAR = "selfavatar";
|
||||
public static final String CONFIG_MVBOX_MOVE = "mvbox_move";
|
||||
public static final String CONFIG_ONLY_FETCH_MVBOX = "only_fetch_mvbox";
|
||||
public static final String CONFIG_BCC_SELF = "bcc_self";
|
||||
public static final String CONFIG_SHOW_EMAILS = "show_emails";
|
||||
public static final String CONFIG_MEDIA_QUALITY = "media_quality";
|
||||
public static final String CONFIG_PROXY_ENABLED = "proxy_enabled";
|
||||
public static final String CONFIG_PROXY_URL = "proxy_url";
|
||||
public static final String CONFIG_PRIVATE_TAG = "private_tag";
|
||||
public static final String CONFIG_STATS_SENDING = "stats_sending";
|
||||
public static final String CONFIG_STATS_ID = "stats_id";
|
||||
public static final String CONFIG_FORCE_ENCRYPTION = "force_encryption";
|
||||
|
||||
public static DcContext getContext(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcContext();
|
||||
@@ -443,6 +446,15 @@ public class DcHelper {
|
||||
return getBlobdirFile(dcContext, filename, ext);
|
||||
}
|
||||
|
||||
public static String copyToBlobdir(Context context, Uri uri, String filename, String ext)
|
||||
throws IOException {
|
||||
String path = getBlobdirFile(getContext(context), filename, ext);
|
||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
|
||||
OutputStream outputStream = new FileOutputStream(path);
|
||||
Util.copy(inputStream, outputStream);
|
||||
return path;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ThreadRecord getThreadRecord(
|
||||
Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
|
||||
@@ -516,16 +528,20 @@ public class DcHelper {
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void showInvalidUnencryptedDialog(Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
public static AlertDialog.Builder prepareInvalidUnencryptedDialog(
|
||||
Context context, AlertDialog.Builder builder) {
|
||||
return builder
|
||||
.setMessage(context.getString(R.string.invalid_unencrypted_explanation))
|
||||
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#howtoe2ee"))
|
||||
.setNegativeButton(
|
||||
R.string.qrscan_title,
|
||||
(d, w) -> context.startActivity(new Intent(context, QrActivity.class)))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
.setCancelable(true);
|
||||
}
|
||||
|
||||
public static void showInvalidUnencryptedDialog(Context context) {
|
||||
prepareInvalidUnencryptedDialog(context, new AlertDialog.Builder(context)).show();
|
||||
}
|
||||
|
||||
public static void openHelp(Context context, String section) {
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.thoughtcrime.securesms.geolocation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ActiveLocationChats {
|
||||
|
||||
private static final String PREFS_NAME = "location_streaming";
|
||||
private static final String KEY_ACTIVE = "active_chat_ids";
|
||||
|
||||
private ActiveLocationChats() {}
|
||||
|
||||
private static SharedPreferences prefs(Context context) {
|
||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a chat. Uses commit() to guarantee the write reaches disk before the process can die to
|
||||
* preserve the superset invariant.
|
||||
*/
|
||||
static void add(Context context, int chatId) {
|
||||
Set<String> current = new HashSet<>(getAll(context));
|
||||
current.add(String.valueOf(chatId));
|
||||
prefs(context).edit().putStringSet(KEY_ACTIVE, current).commit();
|
||||
}
|
||||
|
||||
public static void remove(Context context, int chatId) {
|
||||
Set<String> current = new HashSet<>(getAll(context));
|
||||
current.remove(String.valueOf(chatId));
|
||||
prefs(context).edit().putStringSet(KEY_ACTIVE, current).apply();
|
||||
}
|
||||
|
||||
static void clear(Context context) {
|
||||
prefs(context).edit().remove(KEY_ACTIVE).apply();
|
||||
}
|
||||
|
||||
static Set<Integer> getAllIds(Context context) {
|
||||
Set<Integer> ids = new HashSet<>();
|
||||
for (String s : getAll(context)) {
|
||||
try {
|
||||
ids.add(Integer.parseInt(s));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static Set<String> getAll(Context context) {
|
||||
return prefs(context).getStringSet(KEY_ACTIVE, new HashSet<>());
|
||||
}
|
||||
}
|
||||
+70
-38
@@ -17,13 +17,15 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import org.thoughtcrime.securesms.ConversationListActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
|
||||
public class LocationStreamingService extends Service {
|
||||
|
||||
private static final String TAG = "LocationStreamingService";
|
||||
private static final String TAG = "LocationStreamingSvc";
|
||||
private static final String ACTION_STOP = "org.thoughtcrime.securesms.geolocation.STOP_STREAMING";
|
||||
private static final int NOTIFICATION_ID = 8801;
|
||||
private static final String CHANNEL_ID = "location_streaming";
|
||||
@@ -36,38 +38,63 @@ public class LocationStreamingService extends Service {
|
||||
// static API
|
||||
|
||||
/** Register a chat for location updates, then ensure the service is running. */
|
||||
public static void startSharing(Context context, int chatId, int durationSeconds) {
|
||||
ActiveLocationChats.add(context, chatId);
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, durationSeconds);
|
||||
ContextCompat.startForegroundService(
|
||||
context, new Intent(context, LocationStreamingService.class));
|
||||
}
|
||||
|
||||
/** Unregister a chat. If no chats remain, stop the service. */
|
||||
public static void stopSharing(Context context, int chatId) {
|
||||
ActiveLocationChats.remove(context, chatId);
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
|
||||
if (!DcHelper.getContext(context).isSendingLocationsToChat(0)) {
|
||||
context.stopService(new Intent(context, LocationStreamingService.class));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ensureRunning(Context context) {
|
||||
if (!hasLocationPermission(context)) {
|
||||
for (int chatId : ActiveLocationChats.getAllIds(context)) {
|
||||
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
|
||||
}
|
||||
ActiveLocationChats.clear(context);
|
||||
public static void startSharing(Context context, int accountId, int chatId, int durationSeconds) {
|
||||
try {
|
||||
DcHelper.getRpc(context).sendLocationsToChat(accountId, chatId, durationSeconds);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to start location streaming", e);
|
||||
return;
|
||||
}
|
||||
ContextCompat.startForegroundService(
|
||||
context, new Intent(context, LocationStreamingService.class));
|
||||
}
|
||||
|
||||
/** Unregister a chat. If no chats remain, stop the service. */
|
||||
public static void stopSharing(Context context, int accountId, int chatId) {
|
||||
try {
|
||||
DcHelper.getRpc(context).sendLocationsToChat(accountId, chatId, 0);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to stop location streaming", e);
|
||||
}
|
||||
if (!isAnySharingActive(context)) {
|
||||
context.stopService(new Intent(context, LocationStreamingService.class));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ensureRunning(Context context) {
|
||||
if (!hasLocationPermission(context)) {
|
||||
try {
|
||||
DcHelper.getRpc(context).stopSendingLocations();
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to stop location streaming", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isAnySharingActive(context)) {
|
||||
ContextCompat.startForegroundService(
|
||||
context, new Intent(context, LocationStreamingService.class));
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
private static boolean isAnySharingActive(Context context) {
|
||||
try {
|
||||
Rpc rpc = DcHelper.getRpc(context);
|
||||
for (int accountId : rpc.getAllAccountIds()) {
|
||||
if (rpc.isSendingLocations(accountId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to check location streaming state", e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// lifecycle
|
||||
|
||||
@Override
|
||||
@@ -86,7 +113,7 @@ public class LocationStreamingService extends Service {
|
||||
@Override
|
||||
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
|
||||
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
|
||||
stopAllSharing();
|
||||
stopAllSharing(this);
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
@@ -100,11 +127,12 @@ public class LocationStreamingService extends Service {
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private void stopAllSharing() {
|
||||
for (int chatId : ActiveLocationChats.getAllIds(this)) {
|
||||
DcHelper.getContext(this).sendLocationsToChat(chatId, 0);
|
||||
private static void stopAllSharing(Context context) {
|
||||
try {
|
||||
DcHelper.getRpc(context).stopSendingLocations();
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to stop sending locations", e);
|
||||
}
|
||||
ActiveLocationChats.clear(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -150,16 +178,21 @@ public class LocationStreamingService extends Service {
|
||||
private void publishAndWrite(Location location) {
|
||||
LocationData.getInstance().post(location);
|
||||
|
||||
boolean keepGoing =
|
||||
DcHelper.getContext(this)
|
||||
.setLocation(
|
||||
(float) location.getLatitude(),
|
||||
(float) location.getLongitude(),
|
||||
location.getAccuracy());
|
||||
boolean keepGoing;
|
||||
try {
|
||||
keepGoing =
|
||||
DcHelper.getRpc(this)
|
||||
.setLocation(
|
||||
(float) location.getLatitude(),
|
||||
(float) location.getLongitude(),
|
||||
location.getAccuracy());
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to set location", e);
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "keepGoing: " + keepGoing);
|
||||
|
||||
if (!keepGoing) {
|
||||
stopAllSharing();
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
@@ -188,9 +221,8 @@ public class LocationStreamingService extends Service {
|
||||
NotificationChannel channel =
|
||||
new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.location_streaming_notification_title),
|
||||
getString(R.string.pref_on_demand_location_streaming),
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription(getString(R.string.location_streaming_channel_desc));
|
||||
channel.setShowBadge(false);
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm != null) nm.createNotificationChannel(channel);
|
||||
@@ -209,7 +241,7 @@ public class LocationStreamingService extends Service {
|
||||
PendingIntent.getService(this, 1, stopIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.location_streaming_notification_title))
|
||||
.setContentTitle(getString(R.string.pref_on_demand_location_streaming))
|
||||
.setContentText(getString(R.string.location_streaming_notification_text))
|
||||
.setSmallIcon(R.drawable.ic_location_on_white_24dp)
|
||||
.setOngoing(true)
|
||||
|
||||
@@ -17,14 +17,12 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
@@ -36,6 +34,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import chat.delta.util.ListenableFuture;
|
||||
import chat.delta.util.SettableFuture;
|
||||
@@ -63,22 +62,21 @@ import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.geolocation.ActiveLocationChats;
|
||||
import org.thoughtcrime.securesms.geolocation.LocationStreamingService;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
public class AttachmentManager {
|
||||
|
||||
private static final String TAG = "AttachmentManager";
|
||||
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull Stub<View> attachmentViewStub;
|
||||
private final @NonNull View attachmentView;
|
||||
private final @NonNull AttachmentListener attachmentListener;
|
||||
|
||||
private RemovableEditableMediaView removableMediaView;
|
||||
@@ -99,20 +97,16 @@ public class AttachmentManager {
|
||||
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
|
||||
this.context = activity;
|
||||
this.attachmentListener = listener;
|
||||
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
|
||||
}
|
||||
|
||||
private void inflateStub() {
|
||||
if (!attachmentViewStub.resolved()) {
|
||||
View root = attachmentViewStub.get();
|
||||
|
||||
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
|
||||
this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
|
||||
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
|
||||
this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc);
|
||||
this.vcardView = ViewUtil.findById(root, R.id.attachment_vcard);
|
||||
// this.mapView = ViewUtil.findById(root, R.id.attachment_location);
|
||||
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
|
||||
this.attachmentView = ViewUtil.findById(activity, R.id.attachment_editor);
|
||||
if (this.attachmentView != null) {
|
||||
this.thumbnail = ViewUtil.findById(attachmentView, R.id.attachment_thumbnail);
|
||||
this.audioView = ViewUtil.findById(attachmentView, R.id.attachment_audio);
|
||||
this.documentView = ViewUtil.findById(attachmentView, R.id.attachment_document);
|
||||
this.webxdcView = ViewUtil.findById(attachmentView, R.id.attachment_webxdc);
|
||||
this.vcardView = ViewUtil.findById(attachmentView, R.id.attachment_vcard);
|
||||
// this.mapView = ViewUtil.findById(attachmentView, R.id.attachment_location);
|
||||
this.removableMediaView = ViewUtil.findById(attachmentView, R.id.removable_media_view);
|
||||
|
||||
removableMediaView.addRemoveClickListener(new RemoveButtonListener());
|
||||
removableMediaView.setEditClickListener(new EditButtonListener());
|
||||
@@ -121,10 +115,10 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
public void clear(@NonNull GlideRequests glideRequests, boolean animate) {
|
||||
if (attachmentViewStub.resolved()) {
|
||||
if (this.attachmentView != null) {
|
||||
|
||||
if (animate) {
|
||||
ViewUtil.fadeOut(attachmentViewStub.get(), 200)
|
||||
ViewUtil.fadeOut(attachmentView, 200)
|
||||
.addListener(
|
||||
new ListenableFuture.Listener<Boolean>() {
|
||||
@Override
|
||||
@@ -221,7 +215,6 @@ public class AttachmentManager {
|
||||
}
|
||||
*/
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public ListenableFuture<Boolean> setMedia(
|
||||
@NonNull final GlideRequests glideRequests,
|
||||
@NonNull final Uri uri,
|
||||
@@ -231,149 +224,104 @@ public class AttachmentManager {
|
||||
final int height,
|
||||
final int chatId,
|
||||
AudioPlaybackViewModel playbackViewModel) {
|
||||
inflateStub();
|
||||
|
||||
final SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
|
||||
new AsyncTask<Void, Void, Slide>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
thumbnail.clear(glideRequests);
|
||||
setAttachmentPresent(true);
|
||||
}
|
||||
thumbnail.clear(glideRequests);
|
||||
setAttachmentPresent(true);
|
||||
|
||||
@Override
|
||||
protected @Nullable Slide doInBackground(Void... params) {
|
||||
try {
|
||||
if (msg != null && msg.getType() == DcMsg.DC_MSG_WEBXDC) {
|
||||
return new DocumentSlide(context, msg);
|
||||
} else if (PartAuthority.isLocalUri(uri)) {
|
||||
return getManuallyCalculatedSlideInfo(uri, width, height, msg);
|
||||
} else {
|
||||
Slide result = getContentResolverSlideInfo(uri, width, height, chatId);
|
||||
|
||||
if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height, msg);
|
||||
else return result;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@Nullable final Slide slide) {
|
||||
if (slide == null) {
|
||||
setAttachmentPresent(false);
|
||||
result.set(false);
|
||||
} else if (slide.getFileSize() > 1024 * 1024 * 1024) {
|
||||
// this is only a rough check, videos and images may be recoded
|
||||
// and the core checks more carefully later.
|
||||
setAttachmentPresent(false);
|
||||
Log.w(TAG, "File too large.");
|
||||
Toast.makeText(slide.context, "File too large.", Toast.LENGTH_LONG).show();
|
||||
result.set(false);
|
||||
} else {
|
||||
setSlide(slide);
|
||||
setAttachmentPresent(true);
|
||||
|
||||
if (slide.hasAudio()) {
|
||||
audioView.setPlaybackViewModel(playbackViewModel);
|
||||
audioView.setAudio((AudioSlide) slide);
|
||||
removableMediaView.display(audioView, false);
|
||||
removableMediaView.addRemoveClickListener(
|
||||
v -> {
|
||||
playbackViewModel.stop(audioView.getMsgId());
|
||||
});
|
||||
result.set(true);
|
||||
} else if (slide.isVcard()) {
|
||||
vcardView.setVcard(glideRequests, (VcardSlide) slide, DcHelper.getRpc(context));
|
||||
removableMediaView.display(vcardView, false);
|
||||
} else if (slide.hasDocument()) {
|
||||
if (slide.isWebxdcDocument()) {
|
||||
DcMsg instance =
|
||||
msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId);
|
||||
webxdcView.setWebxdc(instance, context.getString(R.string.webxdc_draft_hint));
|
||||
webxdcView.setWebxdcClickListener(
|
||||
(v, s) -> {
|
||||
WebxdcActivity.openWebxdcActivity(context, instance);
|
||||
});
|
||||
removableMediaView.display(webxdcView, false);
|
||||
Util.runOnBackground(
|
||||
() -> {
|
||||
Slide slide = null;
|
||||
try {
|
||||
if (msg != null && msg.getType() == DcMsg.DC_MSG_WEBXDC) {
|
||||
slide = new DocumentSlide(context, msg);
|
||||
} else if (msg != null
|
||||
&& (msg.getType() == DcMsg.DC_MSG_AUDIO || msg.getType() == DcMsg.DC_MSG_VOICE)) {
|
||||
slide = new AudioSlide(context, msg);
|
||||
} else if (PartAuthority.isLocalUri(uri)) {
|
||||
slide = getManuallyCalculatedSlideInfo(uri, width, height, msg, mediaType, chatId);
|
||||
} else {
|
||||
documentView.setDocument((DocumentSlide) slide);
|
||||
removableMediaView.display(documentView, false);
|
||||
slide = getContentResolverSlideInfo(uri, width, height, chatId, mediaType);
|
||||
if (slide == null) {
|
||||
slide = getManuallyCalculatedSlideInfo(uri, width, height, msg, mediaType, chatId);
|
||||
}
|
||||
}
|
||||
result.set(true);
|
||||
} else {
|
||||
Attachment attachment = slide.asAttachment();
|
||||
result.deferTo(
|
||||
thumbnail.setImageResource(
|
||||
glideRequests, slide, attachment.getWidth(), attachment.getHeight()));
|
||||
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
}
|
||||
final Slide finalSlide = slide;
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
if (finalSlide == null) {
|
||||
setAttachmentPresent(false);
|
||||
result.set(false);
|
||||
} else if (finalSlide.getFileSize() > 1024 * 1024 * 1024) {
|
||||
// this is only a rough check, videos and images may be recoded
|
||||
// and the core checks more carefully later.
|
||||
setAttachmentPresent(false);
|
||||
Log.w(TAG, "File too large.");
|
||||
Toast.makeText(finalSlide.context, "File too large.", Toast.LENGTH_LONG).show();
|
||||
result.set(false);
|
||||
} else {
|
||||
setSlide(finalSlide);
|
||||
setAttachmentPresent(true);
|
||||
|
||||
private @Nullable Slide getContentResolverSlideInfo(
|
||||
Uri uri, int width, int height, int chatId) {
|
||||
if (finalSlide.hasAudio()) {
|
||||
audioView.setPlaybackViewModel(playbackViewModel);
|
||||
audioView.setAudio((AudioSlide) finalSlide);
|
||||
removableMediaView.display(audioView, false);
|
||||
removableMediaView.addRemoveClickListener(
|
||||
v -> playbackViewModel.stop(audioView.getMsgId()));
|
||||
result.set(true);
|
||||
} else if (finalSlide.isVcard()) {
|
||||
vcardView.setVcard(
|
||||
glideRequests, (VcardSlide) finalSlide, DcHelper.getRpc(context));
|
||||
removableMediaView.display(vcardView, false);
|
||||
} else if (finalSlide.hasDocument()) {
|
||||
if (finalSlide.isWebxdcDocument()) {
|
||||
DcMsg instance =
|
||||
msg != null
|
||||
? msg
|
||||
: DcHelper.getContext(context).getMsg(finalSlide.dcMsgId);
|
||||
webxdcView.setWebxdc(instance, context.getString(R.string.webxdc_draft_hint));
|
||||
webxdcView.setWebxdcClickListener(
|
||||
(v, s) -> WebxdcActivity.openWebxdcActivity(context, instance));
|
||||
removableMediaView.display(webxdcView, false);
|
||||
} else {
|
||||
documentView.setDocument((DocumentSlide) finalSlide);
|
||||
removableMediaView.display(documentView, false);
|
||||
}
|
||||
result.set(true);
|
||||
} else {
|
||||
Attachment attachment = finalSlide.asAttachment();
|
||||
result.deferTo(
|
||||
thumbnail.setImageResource(
|
||||
glideRequests,
|
||||
finalSlide,
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight()));
|
||||
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
|
||||
}
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String fileName =
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
|
||||
long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
|
||||
String mimeType = context.getContentResolver().getType(uri);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
|
||||
width = dimens.first;
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
Log.w(
|
||||
TAG,
|
||||
"remote slide with size "
|
||||
+ fileSize
|
||||
+ " took "
|
||||
+ (System.currentTimeMillis() - start)
|
||||
+ "ms");
|
||||
return mediaType.createSlide(
|
||||
context, uri, fileName, mimeType, fileSize, width, height, chatId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull Slide getManuallyCalculatedSlideInfo(
|
||||
Uri uri, int width, int height, @Nullable DcMsg msg) throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
Long mediaSize = null;
|
||||
String fileName = null;
|
||||
String mimeType = null;
|
||||
|
||||
if (msg != null) {
|
||||
fileName = msg.getFilename();
|
||||
mimeType = msg.getFilemime();
|
||||
}
|
||||
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
mediaSize = PartAuthority.getAttachmentSize(context, uri);
|
||||
if (fileName == null) fileName = PartAuthority.getAttachmentFileName(context, uri);
|
||||
if (mimeType == null) mimeType = PartAuthority.getAttachmentContentType(context, uri);
|
||||
}
|
||||
|
||||
if (mediaSize == null) {
|
||||
mediaSize = MediaUtil.getMediaSize(context, uri);
|
||||
}
|
||||
|
||||
if (mimeType == null) {
|
||||
mimeType = MediaUtil.getMimeType(context, uri);
|
||||
}
|
||||
private @Nullable Slide getContentResolverSlideInfo(
|
||||
Uri uri, int width, int height, int chatId, MediaType mediaType) {
|
||||
long start = System.currentTimeMillis();
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String fileName =
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
|
||||
long mediaSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
|
||||
String mimeType = context.getContentResolver().getType(uri);
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
|
||||
@@ -381,14 +329,6 @@ public class AttachmentManager {
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
if (fileName == null) {
|
||||
try {
|
||||
fileName = new File(uri.getPath()).getName();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Could not get file name from uri: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(
|
||||
TAG,
|
||||
"local slide with size "
|
||||
@@ -399,9 +339,61 @@ public class AttachmentManager {
|
||||
return mediaType.createSlide(
|
||||
context, uri, fileName, mimeType, mediaSize, width, height, chatId);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull Slide getManuallyCalculatedSlideInfo(
|
||||
Uri uri, int width, int height, @Nullable DcMsg msg, MediaType mediaType, int chatId)
|
||||
throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
Long mediaSize = null;
|
||||
String fileName = null;
|
||||
String mimeType = null;
|
||||
|
||||
if (msg != null) {
|
||||
fileName = msg.getFilename();
|
||||
mimeType = msg.getFilemime();
|
||||
}
|
||||
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
mediaSize = PartAuthority.getAttachmentSize(context, uri);
|
||||
if (fileName == null) fileName = PartAuthority.getAttachmentFileName(context, uri);
|
||||
if (mimeType == null) mimeType = PartAuthority.getAttachmentContentType(context, uri);
|
||||
}
|
||||
|
||||
if (mediaSize == null) {
|
||||
mediaSize = MediaUtil.getMediaSize(context, uri);
|
||||
}
|
||||
|
||||
if (mimeType == null) {
|
||||
mimeType = MediaUtil.getMimeType(context, uri);
|
||||
}
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
|
||||
width = dimens.first;
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
if (fileName == null) {
|
||||
try {
|
||||
fileName = new File(uri.getPath()).getName();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Could not get file name from uri: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(
|
||||
TAG,
|
||||
"local slide with size "
|
||||
+ mediaSize
|
||||
+ " took "
|
||||
+ (System.currentTimeMillis() - start)
|
||||
+ "ms");
|
||||
return mediaType.createSlide(
|
||||
context, uri, fileName, mimeType, mediaSize, width, height, chatId);
|
||||
}
|
||||
|
||||
// should be called when the attachment manager comes into view again.
|
||||
@@ -447,7 +439,7 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
public static void selectDocument(Activity activity, int requestCode) {
|
||||
selectMediaType(activity, "*/*", null, requestCode);
|
||||
selectMediaType(activity, "*/*", null, requestCode, null, true);
|
||||
}
|
||||
|
||||
public static void selectWebxdc(Activity activity, int requestCode) {
|
||||
@@ -485,22 +477,35 @@ public class AttachmentManager {
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(
|
||||
activity.getString(R.string.perm_explain_access_to_storage_denied))
|
||||
.onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode))
|
||||
.onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode, null, false))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectLocation(Activity activity, int chatId) {
|
||||
Context appContext = activity.getApplicationContext();
|
||||
Rpc rpc = DcHelper.getRpc(appContext);
|
||||
int accountId = DcHelper.getContext(appContext).getAccountId();
|
||||
|
||||
if (DcHelper.getContext(appContext).isSendingLocationsToChat(chatId)) {
|
||||
boolean currentlySharing;
|
||||
try {
|
||||
currentlySharing = rpc.isSendingLocationsToChat(accountId, chatId);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to check location streaming state", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentlySharing) {
|
||||
if (LocationStreamingService.isRunning()) {
|
||||
LocationStreamingService.stopSharing(appContext, chatId);
|
||||
LocationStreamingService.stopSharing(appContext, accountId, chatId);
|
||||
return;
|
||||
}
|
||||
// Stale — service is dead but chat layer still thinks it's sharing.
|
||||
// Clean up this chat and fall through to the fresh start flow.
|
||||
ActiveLocationChats.remove(appContext, chatId);
|
||||
DcHelper.getContext(appContext).sendLocationsToChat(chatId, 0);
|
||||
// Clean up and fall through to the fresh start flow.
|
||||
try {
|
||||
rpc.sendLocationsToChat(accountId, chatId, 0);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to stop stale location streaming", e);
|
||||
}
|
||||
}
|
||||
|
||||
Permissions.with(activity)
|
||||
@@ -514,7 +519,8 @@ public class AttachmentManager {
|
||||
ShareLocationDialog.show(
|
||||
activity,
|
||||
durationInSeconds ->
|
||||
LocationStreamingService.startSharing(appContext, chatId, durationInSeconds));
|
||||
LocationStreamingService.startSharing(
|
||||
appContext, accountId, chatId, durationInSeconds));
|
||||
})
|
||||
.request(
|
||||
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
@@ -596,20 +602,6 @@ public class AttachmentManager {
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectMediaType(
|
||||
Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
||||
selectMediaType(activity, type, extraMimeType, requestCode, null, false);
|
||||
}
|
||||
|
||||
public static void selectMediaType(
|
||||
Activity activity,
|
||||
@NonNull String type,
|
||||
@Nullable String[] extraMimeType,
|
||||
int requestCode,
|
||||
@Nullable Uri initialUri) {
|
||||
selectMediaType(activity, type, extraMimeType, requestCode, initialUri, false);
|
||||
}
|
||||
|
||||
public static void selectMediaType(
|
||||
Activity activity,
|
||||
@NonNull String type,
|
||||
@@ -713,13 +705,31 @@ public class AttachmentManager {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
|
||||
switch (this) {
|
||||
case IMAGE:
|
||||
return new ImageSlide(context, uri, fileName, dataSize, width, height);
|
||||
case GIF:
|
||||
return new GifSlide(context, uri, fileName, dataSize, width, height);
|
||||
case AUDIO:
|
||||
return new AudioSlide(context, uri, dataSize, false, fileName);
|
||||
DcMsg audioMsg = new DcMsg(dcContext, DcMsg.DC_MSG_AUDIO);
|
||||
Attachment audioAttachment =
|
||||
new UriAttachment(
|
||||
uri,
|
||||
null,
|
||||
mimeType,
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
|
||||
dataSize,
|
||||
0,
|
||||
0,
|
||||
fileName,
|
||||
null,
|
||||
false);
|
||||
String audioPath = audioAttachment.getRealPath(context);
|
||||
audioMsg.setFileAndDeduplicate(audioPath, fileName, mimeType);
|
||||
dcContext.setDraft(chatId, audioMsg);
|
||||
return new AudioSlide(context, audioMsg);
|
||||
case VIDEO:
|
||||
return new VideoSlide(context, uri, fileName, dataSize);
|
||||
case DOCUMENT:
|
||||
@@ -727,7 +737,6 @@ public class AttachmentManager {
|
||||
// draft
|
||||
// is set. Therefore we need to create a DcMsg already now.
|
||||
if (fileName != null && fileName.endsWith(".xdc")) {
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC);
|
||||
Attachment attachment =
|
||||
new UriAttachment(
|
||||
@@ -772,6 +781,10 @@ public class AttachmentManager {
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
public boolean isHidden() {
|
||||
return hidden;
|
||||
}
|
||||
|
||||
private void setAttachmentPresent(boolean isPresent) {
|
||||
this.attachmentPresent = isPresent;
|
||||
updateVisibility();
|
||||
@@ -784,9 +797,9 @@ public class AttachmentManager {
|
||||
} else {
|
||||
vis = View.GONE;
|
||||
}
|
||||
if (vis == View.GONE && !attachmentViewStub.resolved()) {
|
||||
if (attachmentView == null) {
|
||||
return;
|
||||
}
|
||||
attachmentViewStub.get().setVisibility(vis);
|
||||
attachmentView.setVisibility(vis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,4 +78,13 @@ public class SlideDeck {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getAudioDraftId() {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.hasAudio() && slide.dcMsgId != 0) {
|
||||
return slide.dcMsgId;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.RingtoneManager;
|
||||
@@ -23,13 +24,18 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.Person;
|
||||
import androidx.core.app.RemoteInput;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashMap;
|
||||
@@ -42,6 +48,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.ConversationListActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShareActivity;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
|
||||
@@ -61,8 +68,18 @@ public class NotificationCenter {
|
||||
private volatile long lastAudibleNotification = 0;
|
||||
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
|
||||
|
||||
// Map<accountId, Map<chatId, lines>, contains the last lines of each chat for each account
|
||||
private final HashMap<Integer, HashMap<Integer, LinkedHashMap<Integer, String>>> inboxes =
|
||||
private static class NotifData {
|
||||
final Person sender;
|
||||
final String text;
|
||||
|
||||
NotifData(Person sender, String text) {
|
||||
this.sender = sender;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
// notification history of each chat for each account
|
||||
private final HashMap<Integer, HashMap<Integer, LinkedHashMap<Integer, NotifData>>> inboxes =
|
||||
new HashMap<>();
|
||||
|
||||
public NotificationCenter(Context context) {
|
||||
@@ -410,31 +427,42 @@ public class NotificationCenter {
|
||||
DcMsg dcMsg = dcContext.getMsg(msgId);
|
||||
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
|
||||
|
||||
String shortLine =
|
||||
DcContact sender = dcContext.getContact(dcMsg.getFromId());
|
||||
String senderName = dcMsg.getSenderName(sender);
|
||||
String personId = accountId + "-" + dcMsg.getFromId();
|
||||
if (dcMsg.getOverrideSenderName() != null) {
|
||||
// we need to treat the contact as a separate Person with different ID
|
||||
// otherwise the name will be overwritten by future notifications
|
||||
personId += "-" + senderName;
|
||||
}
|
||||
String text =
|
||||
privacy.isDisplayMessage()
|
||||
? dcMsg.getSummarytext(2000)
|
||||
: context.getString(R.string.notify_new_message);
|
||||
String shortLine = text;
|
||||
if (dcChat.isMultiUser() && privacy.isDisplayContact()) {
|
||||
shortLine =
|
||||
dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + shortLine;
|
||||
shortLine = senderName + ": " + text;
|
||||
}
|
||||
String tickerLine = shortLine;
|
||||
if (!dcChat.isMultiUser() && privacy.isDisplayContact()) {
|
||||
tickerLine =
|
||||
dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + tickerLine;
|
||||
|
||||
if (dcMsg.getOverrideSenderName() != null) {
|
||||
// There is an "overridden" display name on the message, so, we need to prepend the
|
||||
// display name to the message,
|
||||
// i.e. set the shortLine to be the same as the tickerLine.
|
||||
shortLine = tickerLine;
|
||||
}
|
||||
NotifData notifData =
|
||||
new NotifData(
|
||||
new Person.Builder()
|
||||
.setName(senderName)
|
||||
.setIcon(getAvatarIcon(sender))
|
||||
.setBot(sender.isBot())
|
||||
.setKey(personId)
|
||||
.build(),
|
||||
text);
|
||||
|
||||
String tickerLine = text;
|
||||
if (privacy.isDisplayContact()) {
|
||||
tickerLine = senderName + ": " + text;
|
||||
}
|
||||
|
||||
DcMsg quotedMsg = dcMsg.getQuotedMsg();
|
||||
boolean isMention = dcChat.isMultiUser() && quotedMsg != null && quotedMsg.isOutgoing();
|
||||
|
||||
maybeAddNotification(accountId, dcChat, msgId, shortLine, tickerLine, true, isMention);
|
||||
maybeAddNotification(accountId, dcChat, msgId, notifData, tickerLine, true, isMention);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -450,16 +478,27 @@ public class NotificationCenter {
|
||||
// just do nothing.
|
||||
}
|
||||
|
||||
DcContact sender = dcContext.getContact(contactId);
|
||||
String shortLine =
|
||||
DcContact contact = dcContext.getContact(contactId);
|
||||
String text =
|
||||
context.getString(
|
||||
R.string.reaction_by_other,
|
||||
sender.getDisplayName(),
|
||||
contact.getDisplayName(),
|
||||
reaction,
|
||||
dcMsg.getSummarytext(2000));
|
||||
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
|
||||
|
||||
NotifData notifData =
|
||||
new NotifData(
|
||||
new Person.Builder()
|
||||
.setName(contact.getDisplayName())
|
||||
.setIcon(getAvatarIcon(contact))
|
||||
.setBot(contact.isBot())
|
||||
.setKey(accountId + "-" + contactId)
|
||||
.build(),
|
||||
text);
|
||||
|
||||
maybeAddNotification(
|
||||
accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser());
|
||||
accountId, dcChat, msgId, notifData, text, false, dcChat.isMultiUser());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -473,6 +512,7 @@ public class NotificationCenter {
|
||||
|
||||
DcContext dcContext = context.getDcAccounts().getAccount(accountId);
|
||||
DcMsg dcMsg = dcContext.getMsg(msgId);
|
||||
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
|
||||
DcMsg parentMsg;
|
||||
if (dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) {
|
||||
parentMsg = dcMsg;
|
||||
@@ -486,10 +526,34 @@ public class NotificationCenter {
|
||||
|
||||
JSONObject info = parentMsg.getWebxdcInfo();
|
||||
final String name = JsonUtils.optString(info, "name");
|
||||
String shortLine = name.isEmpty() ? text : (name + ": " + text);
|
||||
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
|
||||
String tickerLine = name.isEmpty() ? text : (name + ": " + text);
|
||||
|
||||
NotifData notifData;
|
||||
if (dcChat.isMultiUser()) {
|
||||
byte[] blob = parentMsg.getWebxdcBlob(JsonUtils.optString(info, "icon"));
|
||||
notifData =
|
||||
new NotifData(
|
||||
new Person.Builder()
|
||||
.setName(name)
|
||||
.setIcon(getAvatarIcon(blob))
|
||||
.setKey(accountId + "-webxdc-" + msgId)
|
||||
.build(),
|
||||
text);
|
||||
} else {
|
||||
DcContact sender = dcContext.getContact(contactId);
|
||||
notifData =
|
||||
new NotifData(
|
||||
new Person.Builder()
|
||||
.setName(sender.getDisplayName())
|
||||
.setIcon(getAvatarIcon(sender))
|
||||
.setBot(sender.isBot())
|
||||
.setKey(accountId + "-" + contactId)
|
||||
.build(),
|
||||
tickerLine);
|
||||
}
|
||||
|
||||
maybeAddNotification(
|
||||
accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser());
|
||||
accountId, dcChat, msgId, notifData, tickerLine, false, dcChat.isMultiUser());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -498,7 +562,7 @@ public class NotificationCenter {
|
||||
int accountId,
|
||||
DcChat dcChat,
|
||||
int msgId,
|
||||
String shortLine,
|
||||
NotifData notifData,
|
||||
String tickerLine,
|
||||
boolean playInChatSound,
|
||||
boolean isMention) {
|
||||
@@ -538,20 +602,23 @@ public class NotificationCenter {
|
||||
// the user may eg. have chosen a different sound
|
||||
String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat);
|
||||
|
||||
LinkedHashMap<Integer, String> messagesForInbox = null;
|
||||
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
|
||||
LinkedHashMap<Integer, NotifData> messagesForInbox = null;
|
||||
if (privacy.isDisplayContact()) {
|
||||
synchronized (inboxes) {
|
||||
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
|
||||
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
|
||||
if (accountInbox == null) {
|
||||
accountInbox = new HashMap<>();
|
||||
inboxes.put(accountId, accountInbox);
|
||||
}
|
||||
LinkedHashMap<Integer, String> messages = accountInbox.get(chatId);
|
||||
LinkedHashMap<Integer, NotifData> messages = accountInbox.get(chatId);
|
||||
if (messages == null) {
|
||||
messages = new LinkedHashMap<>();
|
||||
accountInbox.put(chatId, messages);
|
||||
}
|
||||
messages.put(msgId, shortLine);
|
||||
if (!privacy.isDisplayMessage()) {
|
||||
messages.clear();
|
||||
}
|
||||
messages.put(msgId, notifData);
|
||||
messagesForInbox = new LinkedHashMap<>(messages);
|
||||
}
|
||||
}
|
||||
@@ -564,7 +631,7 @@ public class NotificationCenter {
|
||||
dcContext,
|
||||
dcChat,
|
||||
notificationChannel,
|
||||
shortLine,
|
||||
notifData.text,
|
||||
tickerLine,
|
||||
signal,
|
||||
messagesForInbox,
|
||||
@@ -583,7 +650,7 @@ public class NotificationCenter {
|
||||
String contentText,
|
||||
String ticker,
|
||||
boolean signal,
|
||||
LinkedHashMap<Integer, String> messagesForInbox,
|
||||
LinkedHashMap<Integer, NotifData> messagesForInbox,
|
||||
int messageCount,
|
||||
boolean includeSummary) {
|
||||
try {
|
||||
@@ -658,7 +725,7 @@ public class NotificationCenter {
|
||||
|
||||
NotificationCompat.Action markAsReadAction =
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.check, context.getString(R.string.mark_as_read_short), markReadIntent);
|
||||
R.drawable.check, context.getString(R.string.mark_as_read), markReadIntent);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
NotificationCompat.Action replyAction =
|
||||
@@ -694,14 +761,42 @@ public class NotificationCenter {
|
||||
}
|
||||
}
|
||||
|
||||
// Create inbox style (gets visible if the notification is expanded)
|
||||
if (privacy.isDisplayContact() && privacy.isDisplayMessage() && messagesForInbox != null) {
|
||||
// Create messaging style
|
||||
if (privacy.isDisplayContact() && messagesForInbox != null) {
|
||||
try {
|
||||
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
||||
for (String line : messagesForInbox.values()) {
|
||||
inboxStyle.addLine(line);
|
||||
Intent viewChatIntent = new Intent(context, ShareActivity.class);
|
||||
viewChatIntent.setAction(Intent.ACTION_SEND);
|
||||
viewChatIntent.putExtra(ShareActivity.EXTRA_ACC_ID, dcContext.getAccountId());
|
||||
viewChatIntent.putExtra(ShareActivity.EXTRA_CHAT_ID, dcChat.getId());
|
||||
|
||||
ShortcutInfoCompat shortcut =
|
||||
new ShortcutInfoCompat.Builder(
|
||||
context, "chat-" + dcContext.getAccountId() + "-" + dcChat.getId())
|
||||
.setShortLabel(dcChat.getName())
|
||||
.setLongLived(true)
|
||||
.setIntent(viewChatIntent)
|
||||
.build();
|
||||
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut);
|
||||
builder.setShortcutInfo(shortcut);
|
||||
|
||||
DcContact selfContact = dcContext.getContact(DcContact.DC_CONTACT_ID_SELF);
|
||||
Person self =
|
||||
new Person.Builder()
|
||||
.setName(selfContact.getDisplayName())
|
||||
.setIcon(getAvatarIcon(selfContact))
|
||||
.setKey(accountId + "-" + selfContact.getId())
|
||||
.build();
|
||||
NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle(self);
|
||||
if (dcChat.isMultiUser()) {
|
||||
style.setGroupConversation(true);
|
||||
style.setConversationTitle(dcChat.getName());
|
||||
}
|
||||
builder.setStyle(inboxStyle);
|
||||
for (Map.Entry<Integer, NotifData> msgEntry : messagesForInbox.entrySet()) {
|
||||
long timestamp_ms = dcContext.getMsg(msgEntry.getKey()).getSortTimestamp() * 1000;
|
||||
NotifData notifData = msgEntry.getValue();
|
||||
style.addMessage(notifData.text, timestamp_ms, notifData.sender);
|
||||
}
|
||||
builder.setStyle(style);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
@@ -748,7 +843,7 @@ public class NotificationCenter {
|
||||
|
||||
@WorkerThread
|
||||
private void rebuildNotification(
|
||||
int accountId, int chatId, LinkedHashMap<Integer, String> messages) {
|
||||
int accountId, int chatId, LinkedHashMap<Integer, NotifData> messages) {
|
||||
try {
|
||||
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accountId);
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
@@ -766,9 +861,9 @@ public class NotificationCenter {
|
||||
// Get the latest message ID (last entry in LinkedHashMap)
|
||||
Integer latestMsgId = null;
|
||||
String lastLine = null;
|
||||
for (Map.Entry<Integer, String> entry : messages.entrySet()) {
|
||||
for (Map.Entry<Integer, NotifData> entry : messages.entrySet()) {
|
||||
latestMsgId = entry.getKey();
|
||||
lastLine = entry.getValue();
|
||||
lastLine = entry.getValue().text;
|
||||
}
|
||||
if (latestMsgId == null || lastLine == null) {
|
||||
return;
|
||||
@@ -797,8 +892,35 @@ public class NotificationCenter {
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap getAvatar(DcChat dcChat) {
|
||||
Recipient recipient = new Recipient(context, dcChat);
|
||||
private static @Nullable IconCompat getAvatarIcon(byte[] blob) {
|
||||
if (blob == null) {
|
||||
return null;
|
||||
}
|
||||
ByteArrayInputStream is = new ByteArrayInputStream(blob);
|
||||
BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon");
|
||||
Bitmap bitmap = drawable.getBitmap();
|
||||
return IconCompat.createWithBitmap(bitmap);
|
||||
}
|
||||
|
||||
private @Nullable IconCompat getAvatarIcon(DcChat dcChat) {
|
||||
Bitmap avatar = getAvatar(dcChat);
|
||||
return avatar != null ? IconCompat.createWithBitmap(avatar) : null;
|
||||
}
|
||||
|
||||
private @Nullable IconCompat getAvatarIcon(DcContact dcContact) {
|
||||
Bitmap avatar = getAvatar(dcContact);
|
||||
return avatar != null ? IconCompat.createWithBitmap(avatar) : null;
|
||||
}
|
||||
|
||||
private @Nullable Bitmap getAvatar(DcChat dcChat) {
|
||||
return getAvatar(new Recipient(context, dcChat));
|
||||
}
|
||||
|
||||
private @Nullable Bitmap getAvatar(DcContact dcContact) {
|
||||
return getAvatar(new Recipient(context, dcContact));
|
||||
}
|
||||
|
||||
private @Nullable Bitmap getAvatar(Recipient recipient) {
|
||||
try {
|
||||
Drawable drawable;
|
||||
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
|
||||
@@ -837,12 +959,12 @@ public class NotificationCenter {
|
||||
public void removeNotification(int accountId, int chatId, int msgId) {
|
||||
boolean shouldCancelNotification = false;
|
||||
boolean removeSummary = false;
|
||||
LinkedHashMap<Integer, String> remainingMessages = null;
|
||||
LinkedHashMap<Integer, NotifData> remainingMessages = null;
|
||||
|
||||
synchronized (inboxes) {
|
||||
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
|
||||
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
|
||||
if (accountInbox != null) {
|
||||
LinkedHashMap<Integer, String> messages = accountInbox.get(chatId);
|
||||
LinkedHashMap<Integer, NotifData> messages = accountInbox.get(chatId);
|
||||
if (messages != null) {
|
||||
messages.remove(msgId);
|
||||
|
||||
@@ -875,7 +997,7 @@ public class NotificationCenter {
|
||||
public void removeNotifications(int accountId, int chatId) {
|
||||
boolean removeSummary;
|
||||
synchronized (inboxes) {
|
||||
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
|
||||
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
|
||||
if (accountInbox == null) {
|
||||
accountInbox = new HashMap<>();
|
||||
}
|
||||
@@ -901,7 +1023,7 @@ public class NotificationCenter {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
String tag = String.valueOf(accountId);
|
||||
synchronized (inboxes) {
|
||||
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
|
||||
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
|
||||
notificationManager.cancel(tag, ID_MSG_SUMMARY);
|
||||
if (accountInbox != null) {
|
||||
for (Integer chatId : accountInbox.keySet()) {
|
||||
|
||||
@@ -3,9 +3,6 @@ package org.thoughtcrime.securesms.preferences;
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.text.InputType.TYPE_TEXT_VARIATION_URI;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_BCC_SELF;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_MVBOX_MOVE;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_ONLY_FETCH_MVBOX;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_SHOW_EMAILS;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_STATS_SENDING;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -22,7 +19,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -40,17 +36,13 @@ import org.thoughtcrime.securesms.relay.RelayListActivity;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ScreenLockUtil;
|
||||
import org.thoughtcrime.securesms.util.StreamUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
implements DcEventCenter.DcEventDelegate {
|
||||
private static final String TAG = "AdvancedPreferenceFragment";
|
||||
|
||||
private ListPreference showEmails;
|
||||
CheckBoxPreference selfReportingCheckbox;
|
||||
CheckBoxPreference multiDeviceCheckbox;
|
||||
CheckBoxPreference mvboxMoveCheckbox;
|
||||
CheckBoxPreference onlyFetchMvboxCheckbox;
|
||||
private ActivityResultLauncher<Intent> screenLockLauncher;
|
||||
|
||||
@Override
|
||||
@@ -66,16 +58,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
}
|
||||
});
|
||||
|
||||
showEmails = (ListPreference) this.findPreference("pref_show_emails");
|
||||
if (showEmails != null) {
|
||||
showEmails.setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
updateListSummary(preference, newValue);
|
||||
dcContext.setConfigInt(CONFIG_SHOW_EMAILS, Util.objectToInt(newValue));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
multiDeviceCheckbox = (CheckBoxPreference) this.findPreference("pref_bcc_self");
|
||||
if (multiDeviceCheckbox != null) {
|
||||
multiDeviceCheckbox.setOnPreferenceChangeListener(
|
||||
@@ -100,40 +82,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
});
|
||||
}
|
||||
|
||||
mvboxMoveCheckbox = (CheckBoxPreference) this.findPreference("pref_mvbox_move");
|
||||
if (mvboxMoveCheckbox != null) {
|
||||
mvboxMoveCheckbox.setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
boolean enabled = (Boolean) newValue;
|
||||
dcContext.setConfigInt(CONFIG_MVBOX_MOVE, enabled ? 1 : 0);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
onlyFetchMvboxCheckbox = this.findPreference("pref_only_fetch_mvbox");
|
||||
if (onlyFetchMvboxCheckbox != null) {
|
||||
onlyFetchMvboxCheckbox.setOnPreferenceChangeListener(
|
||||
((preference, newValue) -> {
|
||||
final boolean enabled = (Boolean) newValue;
|
||||
if (enabled) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.pref_imap_folder_warn_disable_defaults)
|
||||
.setPositiveButton(
|
||||
R.string.ok,
|
||||
(dialogInterface, i) -> {
|
||||
dcContext.setConfigInt(CONFIG_ONLY_FETCH_MVBOX, 1);
|
||||
((CheckBoxPreference) preference).setChecked(true);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
return false;
|
||||
} else {
|
||||
dcContext.setConfigInt(CONFIG_ONLY_FETCH_MVBOX, 0);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Preference submitDebugLog = this.findPreference("pref_view_log");
|
||||
if (submitDebugLog != null) {
|
||||
submitDebugLog.setOnPreferenceClickListener(new ViewLogListener());
|
||||
@@ -189,10 +137,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
if (dcContext.isChatmail()) {
|
||||
findPreference("pref_category_legacy").setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -207,14 +151,8 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
|
||||
((ApplicationPreferencesActivity) requireActivity()).getSupportActionBar())
|
||||
.setTitle(R.string.menu_advanced);
|
||||
|
||||
String value = Integer.toString(dcContext.getConfigInt("show_emails"));
|
||||
showEmails.setValue(value);
|
||||
updateListSummary(showEmails, value);
|
||||
|
||||
selfReportingCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_STATS_SENDING));
|
||||
multiDeviceCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_BCC_SELF));
|
||||
mvboxMoveCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_MVBOX_MOVE));
|
||||
onlyFetchMvboxCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_ONLY_FETCH_MVBOX));
|
||||
}
|
||||
|
||||
protected File copyToCacheDir(Uri uri) throws IOException {
|
||||
|
||||
+12
@@ -39,6 +39,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
||||
private CheckBoxPreference ignoreBattery;
|
||||
private CheckBoxPreference notificationsEnabled;
|
||||
private CheckBoxPreference mentionNotifEnabled;
|
||||
private CheckBoxPreference notifyCalls;
|
||||
private CheckBoxPreference reliableService;
|
||||
private ActivityResultLauncher<Intent> ringtonePickerLauncher;
|
||||
|
||||
@@ -137,6 +138,16 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
notifyCalls = this.findPreference("pref_notify_calls");
|
||||
if (notifyCalls != null) {
|
||||
notifyCalls.setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
boolean enabled = (Boolean) newValue;
|
||||
dcContext.setConfig("who_can_call_me", enabled ? "1" : "2");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,6 +167,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
||||
notificationsEnabled.setChecked(!dcContext.isMuted());
|
||||
notificationsEnabled.setSummary(getSummary(getContext(), false));
|
||||
mentionNotifEnabled.setChecked(dcContext.isMentionsEnabled());
|
||||
notifyCalls.setChecked(!"2".equals(dcContext.getConfig("who_can_call_me")));
|
||||
|
||||
// set without altering "unset" state of the preference
|
||||
reliableService.setOnPreferenceChangeListener(null);
|
||||
|
||||
+6
-45
@@ -26,7 +26,6 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
private CheckBoxPreference readReceiptsCheckbox;
|
||||
|
||||
private ListPreference autoDelDevice;
|
||||
private ListPreference autoDelServer;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
@@ -39,13 +38,7 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
.setOnPreferenceClickListener(new BlockedContactsClickListener());
|
||||
|
||||
autoDelDevice = findPreference("autodel_device");
|
||||
autoDelDevice.setOnPreferenceChangeListener(new AutodelChangeListener("delete_device_after"));
|
||||
|
||||
autoDelServer = findPreference("autodel_server");
|
||||
autoDelServer.setOnPreferenceChangeListener(new AutodelChangeListener("delete_server_after"));
|
||||
if (dcContext.isChatmail()) {
|
||||
autoDelServer.setVisible(false);
|
||||
}
|
||||
autoDelDevice.setOnPreferenceChangeListener(new AutodelChangeListener());
|
||||
|
||||
Preference screenSecurity = this.findPreference(Prefs.SCREEN_SECURITY_PREF);
|
||||
screenSecurity.setOnPreferenceChangeListener(new ScreenShotSecurityListener());
|
||||
@@ -68,16 +61,7 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
}
|
||||
|
||||
private void initAutodelFromCore() {
|
||||
String value = Integer.toString(dcContext.getConfigInt("delete_server_after"));
|
||||
autoDelServer.setValue(value);
|
||||
updateListSummary(
|
||||
autoDelServer,
|
||||
value,
|
||||
(value.equals("0") || dcContext.isChatmail())
|
||||
? null
|
||||
: getString(R.string.autodel_server_enabled_hint));
|
||||
|
||||
value = Integer.toString(dcContext.getConfigInt("delete_device_after"));
|
||||
String value = Integer.toString(dcContext.getConfigInt("delete_device_after"));
|
||||
autoDelDevice.setValue(value);
|
||||
updateListSummary(autoDelDevice, value);
|
||||
}
|
||||
@@ -109,19 +93,14 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
}
|
||||
|
||||
private class AutodelChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
private final String coreKey;
|
||||
|
||||
AutodelChangeListener(String coreKey) {
|
||||
this.coreKey = coreKey;
|
||||
}
|
||||
private final String coreKey = "delete_device_after";
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
int timeout = Util.objectToInt(newValue);
|
||||
Context context = preference.getContext();
|
||||
boolean fromServer = coreKey.equals("delete_server_after");
|
||||
if (timeout > 0 && !(fromServer && dcContext.isChatmail())) {
|
||||
int delCount = DcHelper.getContext(context).estimateDeletionCount(fromServer, timeout);
|
||||
if (timeout > 0) {
|
||||
int delCount = DcHelper.getContext(context).estimateDeletionCount(false, timeout);
|
||||
|
||||
View gl = View.inflate(getActivity(), R.layout.dialog_with_checkbox, null);
|
||||
CheckBox confirmCheckbox = gl.findViewById(R.id.dialog_checkbox);
|
||||
@@ -132,8 +111,7 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
// "OK" and "Cancel" buttons would not be show. So, put the message into our custom view:
|
||||
msg.setText(
|
||||
String.format(
|
||||
context.getString(
|
||||
fromServer ? R.string.autodel_server_ask : R.string.autodel_device_ask),
|
||||
context.getString(R.string.autodel_device_ask),
|
||||
delCount,
|
||||
getSelectedSummary(preference, newValue)));
|
||||
confirmCheckbox.setText(R.string.autodel_confirm);
|
||||
@@ -158,23 +136,6 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
// :)
|
||||
.setOnCancelListener(dialog -> initAutodelFromCore())
|
||||
.show();
|
||||
} else if (fromServer
|
||||
&& timeout
|
||||
== 1 /*at once, using a constant that cannot be used in .xml would weaken grep ability*/) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.autodel_server_warn_multi_device_title)
|
||||
.setMessage(R.string.autodel_server_warn_multi_device)
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(dialog, whichButton) -> {
|
||||
dcContext.setConfigInt(coreKey, timeout);
|
||||
initAutodelFromCore();
|
||||
})
|
||||
.setNegativeButton(
|
||||
android.R.string.cancel, (dialog, whichButton) -> initAutodelFromCore())
|
||||
.setCancelable(true)
|
||||
.setOnCancelListener(dialog -> initAutodelFromCore())
|
||||
.show();
|
||||
} else {
|
||||
updateListSummary(preference, newValue);
|
||||
dcContext.setConfigInt(coreKey, timeout);
|
||||
|
||||
@@ -15,10 +15,12 @@ import android.view.View;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.NotFoundException;
|
||||
@@ -48,7 +50,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
private static final int TAB_SCAN = 1;
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private ViewPager2 viewPager;
|
||||
private QrShowFragment qrShowFragment;
|
||||
private boolean scanRelay;
|
||||
|
||||
@@ -68,7 +70,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
qrShowFragment = new QrShowFragment(this);
|
||||
tabLayout = ViewUtil.findById(this, R.id.tab_layout);
|
||||
viewPager = ViewUtil.findById(this, R.id.pager);
|
||||
ProfilePagerAdapter adapter = new ProfilePagerAdapter(this, getSupportFragmentManager());
|
||||
ProfilePagerAdapter adapter = new ProfilePagerAdapter(this);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
setSupportActionBar(ViewUtil.findById(this, R.id.toolbar));
|
||||
@@ -76,29 +78,24 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
getSupportActionBar().setTitle(scanRelay ? R.string.add_transport : R.string.menu_new_contact);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
viewPager.setCurrentItem(scanRelay ? TAB_SCAN : TAB_SHOW);
|
||||
viewPager.setCurrentItem(scanRelay ? TAB_SCAN : TAB_SHOW, false);
|
||||
if (scanRelay) tabLayout.setVisibility(View.GONE);
|
||||
|
||||
viewPager.addOnPageChangeListener(
|
||||
new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(
|
||||
int position, float positionOffset, int positionOffsetPixels) {}
|
||||
|
||||
viewPager.registerOnPageChangeCallback(
|
||||
new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
QrActivity.this.invalidateOptionsMenu();
|
||||
checkPermissions(position, adapter, viewPager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
});
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
new TabLayoutMediator(
|
||||
tabLayout, viewPager, (tab, position) -> tab.setText(adapter.getPageTitle(position)))
|
||||
.attach();
|
||||
}
|
||||
|
||||
private void checkPermissions(int position, ProfilePagerAdapter adapter, ViewPager viewPager) {
|
||||
private void checkPermissions(int position, ProfilePagerAdapter adapter, ViewPager2 viewPager) {
|
||||
if (position == TAB_SCAN) {
|
||||
Permissions.with(QrActivity.this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
@@ -106,7 +103,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
|
||||
.onAllGranted(
|
||||
() ->
|
||||
((QrScanFragment) adapter.getItem(TAB_SCAN))
|
||||
((QrScanFragment) adapter.createFragment(TAB_SCAN))
|
||||
.handleQrScanWithPermissions(QrActivity.this))
|
||||
.onAnyDenied(
|
||||
() -> {
|
||||
@@ -124,10 +121,12 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
DcContext dcContext = DcHelper.getContext(this);
|
||||
|
||||
menu.clear();
|
||||
getMenuInflater().inflate(R.menu.qr_show, menu);
|
||||
menu.findItem(R.id.new_classic_contact)
|
||||
.setVisible(!scanRelay && !DcHelper.getContext(this).isChatmail());
|
||||
.setVisible(!scanRelay && dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0);
|
||||
|
||||
Util.redMenuItem(menu, R.id.withdraw);
|
||||
if (tabLayout.getSelectedTabPosition() == TAB_SCAN) {
|
||||
@@ -228,47 +227,28 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
viewPager.setCurrentItem(TAB_SCAN);
|
||||
}
|
||||
|
||||
private class ProfilePagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final QrActivity activity;
|
||||
|
||||
ProfilePagerAdapter(QrActivity activity, FragmentManager fragmentManager) {
|
||||
super(fragmentManager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.activity = activity;
|
||||
private class ProfilePagerAdapter extends FragmentStateAdapter {
|
||||
ProfilePagerAdapter(FragmentActivity activity) {
|
||||
super(activity);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Fragment fragment;
|
||||
|
||||
switch (position) {
|
||||
case TAB_SHOW:
|
||||
fragment = activity.qrShowFragment;
|
||||
break;
|
||||
|
||||
default:
|
||||
fragment = new QrScanFragment();
|
||||
break;
|
||||
public Fragment createFragment(int position) {
|
||||
if (position == TAB_SHOW) {
|
||||
qrShowFragment = new QrShowFragment(QrActivity.this);
|
||||
return qrShowFragment;
|
||||
}
|
||||
|
||||
return fragment;
|
||||
return new QrScanFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case TAB_SHOW:
|
||||
return getString(R.string.qrshow_title);
|
||||
|
||||
default:
|
||||
return getString(R.string.qrscan_title);
|
||||
}
|
||||
private CharSequence getPageTitle(int position) {
|
||||
return getString(position == TAB_SHOW ? R.string.qrshow_title : R.string.qrscan_title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +314,12 @@ public class QrCodeHandler {
|
||||
|
||||
private void showFingerprintOrQrSuccess(
|
||||
AlertDialog.Builder builder, DcLot qrParsed, String name) {
|
||||
if (qrParsed.getState() == DcContext.DC_QR_ADDR
|
||||
&& dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 1) {
|
||||
DcHelper.prepareInvalidUnencryptedDialog(activity, builder);
|
||||
return;
|
||||
}
|
||||
|
||||
@StringRes
|
||||
int resId =
|
||||
qrParsed.getState() == DcContext.DC_QR_ADDR
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
@@ -72,9 +73,13 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
private ProgressDialog progressDialog;
|
||||
private boolean cancelled = false;
|
||||
|
||||
private View imapFolderLayout;
|
||||
private boolean showImapFolder = false;
|
||||
|
||||
Spinner imapSecurity;
|
||||
Spinner smtpSecurity;
|
||||
Spinner certCheck;
|
||||
private SwitchCompat enforceE2eeSwitch;
|
||||
|
||||
Rpc rpc;
|
||||
int accId;
|
||||
@@ -100,9 +105,11 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
|
||||
advancedGroup = findViewById(R.id.advanced_group);
|
||||
advancedIcon = findViewById(R.id.advanced_icon);
|
||||
imapFolderLayout = findViewById(R.id.imap_folder);
|
||||
TextView advancedTextView = findViewById(R.id.advanced_text);
|
||||
TextInputEditText imapServerInput = findViewById(R.id.imap_server_text);
|
||||
TextInputEditText imapPortInput = findViewById(R.id.imap_port_text);
|
||||
TextInputEditText imapFolderInput = findViewById(R.id.imap_folder_text);
|
||||
TextInputEditText smtpServerInput = findViewById(R.id.smtp_server_text);
|
||||
TextInputEditText smtpPortInput = findViewById(R.id.smtp_port_text);
|
||||
TextView viewLogText = findViewById(R.id.view_log_button);
|
||||
@@ -110,6 +117,7 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
imapSecurity = findViewById(R.id.imap_security);
|
||||
smtpSecurity = findViewById(R.id.smtp_security);
|
||||
certCheck = findViewById(R.id.cert_check);
|
||||
enforceE2eeSwitch = findViewById(R.id.enforce_e2ee_switch);
|
||||
|
||||
String addr = getIntent().getStringExtra(EXTRA_ADDR);
|
||||
EnteredLoginParam config = null;
|
||||
@@ -173,6 +181,10 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
boolean expandAdvanced = false;
|
||||
int intVal;
|
||||
|
||||
intVal = getContext(this).getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION);
|
||||
enforceE2eeSwitch.setChecked(intVal == 1);
|
||||
expandAdvanced = expandAdvanced || intVal == 0;
|
||||
|
||||
if (config != null) { // configured
|
||||
emailInput.setText(config.addr);
|
||||
if (!TextUtils.isEmpty(config.addr)) {
|
||||
@@ -190,6 +202,12 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
if (config.imapPort != null) imapPortInput.setText(config.imapPort.toString());
|
||||
expandAdvanced = expandAdvanced || config.imapPort != null;
|
||||
|
||||
showImapFolder = !TextUtils.isEmpty(config.imapFolder);
|
||||
if (showImapFolder) {
|
||||
imapFolderInput.setText(config.imapFolder);
|
||||
expandAdvanced = true;
|
||||
}
|
||||
|
||||
intVal = socketSecurityToInt(config.imapSecurity);
|
||||
imapSecurity.setSelection(ViewUtil.checkBounds(intVal, imapSecurity));
|
||||
expandAdvanced = expandAdvanced || intVal != 0;
|
||||
@@ -386,9 +404,11 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
boolean advancedViewVisible = advancedGroup.getVisibility() == View.VISIBLE;
|
||||
if (advancedViewVisible) {
|
||||
advancedGroup.setVisibility(View.GONE);
|
||||
imapFolderLayout.setVisibility(View.GONE);
|
||||
advancedIcon.setRotation(45);
|
||||
} else {
|
||||
advancedGroup.setVisibility(View.VISIBLE);
|
||||
imapFolderLayout.setVisibility(showImapFolder ? View.VISIBLE : View.GONE);
|
||||
advancedIcon.setRotation(0);
|
||||
}
|
||||
}
|
||||
@@ -491,6 +511,7 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
param.password = getParam(R.id.password_text, false);
|
||||
param.imapServer = getParam(R.id.imap_server_text, true);
|
||||
param.imapPort = Util.objectToInt(getParam(R.id.imap_port_text, true));
|
||||
param.imapFolder = getParam(R.id.imap_folder_text, true);
|
||||
param.imapSecurity = socketSecurityFromInt(imapSecurity.getSelectedItemPosition());
|
||||
param.imapUser = getParam(R.id.imap_login_text, false);
|
||||
param.smtpServer = getParam(R.id.smtp_server_text, true);
|
||||
@@ -500,9 +521,12 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
param.smtpPassword = getParam(R.id.smtp_password_text, false);
|
||||
param.certificateChecks = certificateChecksFromInt(certCheck.getSelectedItemPosition());
|
||||
|
||||
final String forceEncryption = enforceE2eeSwitch.isChecked() ? "1" : "0";
|
||||
|
||||
new Thread(
|
||||
() -> {
|
||||
try {
|
||||
rpc.setConfig(accId, DcHelper.CONFIG_FORCE_ENCRYPTION, forceEncryption);
|
||||
rpc.addOrUpdateTransport(accId, param);
|
||||
DcHelper.getEventCenter(this).endCaptureNextError();
|
||||
progressDialog.dismiss();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.search;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcLot;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class QrInviteData {
|
||||
|
||||
private final String displayTitle;
|
||||
private final String displaySubtitle;
|
||||
private final String rawQrString;
|
||||
private final int contactId;
|
||||
|
||||
private QrInviteData(
|
||||
@NonNull String displayTitle,
|
||||
@NonNull String displaySubtitle,
|
||||
@NonNull String rawQrString,
|
||||
int contactId) {
|
||||
this.displayTitle = displayTitle;
|
||||
this.displaySubtitle = displaySubtitle;
|
||||
this.rawQrString = rawQrString;
|
||||
this.contactId = contactId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static QrInviteData from(
|
||||
@NonNull Context context,
|
||||
@NonNull DcContext dcContext,
|
||||
@NonNull DcLot qrParsed,
|
||||
@NonNull String rawQrString) {
|
||||
int state = qrParsed.getState();
|
||||
String title;
|
||||
String subtitle;
|
||||
int contactId = 0;
|
||||
|
||||
switch (state) {
|
||||
case DcContext.DC_QR_ASK_VERIFYCONTACT:
|
||||
case DcContext.DC_QR_FPR_OK:
|
||||
case DcContext.DC_QR_ADDR:
|
||||
contactId = qrParsed.getId();
|
||||
DcContact contact = dcContext.getContact(contactId);
|
||||
title = contact.getDisplayName();
|
||||
subtitle = context.getString(R.string.start_chat);
|
||||
break;
|
||||
case DcContext.DC_QR_ASK_VERIFYGROUP:
|
||||
title = qrParsed.getText1();
|
||||
subtitle = context.getString(R.string.join_group);
|
||||
break;
|
||||
case DcContext.DC_QR_ASK_JOIN_BROADCAST:
|
||||
title = qrParsed.getText1();
|
||||
subtitle = context.getString(R.string.join_channel);
|
||||
break;
|
||||
case DcContext.DC_QR_ACCOUNT:
|
||||
case DcContext.DC_QR_LOGIN:
|
||||
title = qrParsed.getText1();
|
||||
subtitle = context.getString(R.string.add_transport);
|
||||
break;
|
||||
case DcContext.DC_QR_PROXY:
|
||||
title = qrParsed.getText1();
|
||||
subtitle = context.getString(R.string.proxy_use_proxy);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return new QrInviteData(title, subtitle, rawQrString, contactId);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getDisplayTitle() {
|
||||
return displayTitle;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getDisplaySubtitle() {
|
||||
return displaySubtitle;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getRawQrString() {
|
||||
return rawQrString;
|
||||
}
|
||||
|
||||
public int getContactId() {
|
||||
return contactId;
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,14 @@ public class SearchFragment extends BaseConversationListFragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvitationClicked(@NonNull String rawQrString) {
|
||||
ConversationListActivity conversationList = (ConversationListActivity) getActivity();
|
||||
if (conversationList != null) {
|
||||
conversationList.handleQrFromSearch(rawQrString);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateSearchQuery(@NonNull String query) {
|
||||
if (viewModel != null) {
|
||||
viewModel.updateQuery(query);
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
class SearchListAdapter
|
||||
extends BaseConversationListAdapter<SearchListAdapter.SearchResultViewHolder>
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<SearchListAdapter.HeaderViewHolder> {
|
||||
private static final int TYPE_QR_INVITE = 0;
|
||||
private static final int TYPE_CHATS = 1;
|
||||
private static final int TYPE_CONTACTS = 2;
|
||||
private static final int TYPE_MESSAGES = 3;
|
||||
@@ -57,6 +58,14 @@ class SearchListAdapter
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SearchResultViewHolder holder, int position) {
|
||||
if (isQrInvitePosition(position)) {
|
||||
QrInviteData inviteData = searchResult.getQrInviteData();
|
||||
if (inviteData != null) {
|
||||
holder.bind(inviteData, glideRequests, eventListener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DcChatlist.Item conversationResult = getConversationResult(position);
|
||||
|
||||
if (conversationResult != null) {
|
||||
@@ -97,7 +106,9 @@ class SearchListAdapter
|
||||
|
||||
@Override
|
||||
public long getHeaderId(int position) {
|
||||
if (getConversationResult(position) != null) {
|
||||
if (isQrInvitePosition(position)) {
|
||||
return TYPE_QR_INVITE;
|
||||
} else if (getConversationResult(position) != null) {
|
||||
return TYPE_CHATS;
|
||||
} else if (getContactResult(position) != null) {
|
||||
return TYPE_CONTACTS;
|
||||
@@ -116,33 +127,39 @@ class SearchListAdapter
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
|
||||
int headerType = (int) getHeaderId(position);
|
||||
int textId = R.plurals.n_messages;
|
||||
int count = 1;
|
||||
boolean maybeLimitedTo1000 = false;
|
||||
String title;
|
||||
|
||||
switch (headerType) {
|
||||
case TYPE_CHATS:
|
||||
textId = R.plurals.n_chats;
|
||||
count = searchResult.getChats().getCnt();
|
||||
break;
|
||||
case TYPE_CONTACTS:
|
||||
textId = R.plurals.n_contacts;
|
||||
count = searchResult.getContacts().length;
|
||||
break;
|
||||
case TYPE_MESSAGES:
|
||||
textId = R.plurals.n_messages;
|
||||
count = searchResult.getMessages().length;
|
||||
maybeLimitedTo1000 =
|
||||
count == 1000; // a count of 1000 results may be limited, see documentation of
|
||||
// dc_search_msgs()
|
||||
break;
|
||||
}
|
||||
if (headerType == TYPE_QR_INVITE) {
|
||||
title = context.getString(R.string.link);
|
||||
} else {
|
||||
int textId = R.plurals.n_messages;
|
||||
int count = 1;
|
||||
boolean maybeLimitedTo1000 = false;
|
||||
|
||||
String title = context.getResources().getQuantityString(textId, count, count);
|
||||
if (maybeLimitedTo1000) {
|
||||
title =
|
||||
title.replace(
|
||||
"000", "000+"); // skipping the first digit allows formattings as "1.000" or "1,000"
|
||||
switch (headerType) {
|
||||
case TYPE_CHATS:
|
||||
textId = R.plurals.n_chats;
|
||||
count = searchResult.getChats().getCnt();
|
||||
break;
|
||||
case TYPE_CONTACTS:
|
||||
textId = R.plurals.n_contacts;
|
||||
count = searchResult.getContacts().length;
|
||||
break;
|
||||
case TYPE_MESSAGES:
|
||||
textId = R.plurals.n_messages;
|
||||
count = searchResult.getMessages().length;
|
||||
maybeLimitedTo1000 =
|
||||
count == 1000; // a count of 1000 results may be limited, see documentation of
|
||||
// dc_search_msgs()
|
||||
break;
|
||||
}
|
||||
|
||||
title = context.getResources().getQuantityString(textId, count, count);
|
||||
if (maybeLimitedTo1000) {
|
||||
title =
|
||||
title.replace(
|
||||
"000", "000+"); // skipping the first digit allows formattings as "1.000" or "1,000"
|
||||
}
|
||||
}
|
||||
viewHolder.bind(title);
|
||||
}
|
||||
@@ -160,10 +177,19 @@ class SearchListAdapter
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private int getQrInviteCount() {
|
||||
return searchResult.getQrInviteData() != null ? 1 : 0;
|
||||
}
|
||||
|
||||
private boolean isQrInvitePosition(int position) {
|
||||
return position == 0 && searchResult.getQrInviteData() != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DcChatlist.Item getConversationResult(int position) {
|
||||
if (position < searchResult.getChats().getCnt()) {
|
||||
return searchResult.getChats().getItem(position);
|
||||
int offset = position - getQrInviteCount();
|
||||
if (offset >= 0 && offset < searchResult.getChats().getCnt()) {
|
||||
return searchResult.getChats().getItem(offset);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -185,7 +211,7 @@ class SearchListAdapter
|
||||
}
|
||||
|
||||
private int getFirstContactIndex() {
|
||||
return searchResult.getChats().getCnt();
|
||||
return getQrInviteCount() + searchResult.getChats().getCnt();
|
||||
}
|
||||
|
||||
private int getFirstMessageIndex() {
|
||||
@@ -200,6 +226,8 @@ class SearchListAdapter
|
||||
void onContactClicked(@NonNull DcContact contact);
|
||||
|
||||
void onMessageClicked(@NonNull DcMsg message);
|
||||
|
||||
void onInvitationClicked(@NonNull String rawQrString);
|
||||
}
|
||||
|
||||
static class SearchResultViewHolder extends RecyclerView.ViewHolder {
|
||||
@@ -257,9 +285,20 @@ class SearchListAdapter
|
||||
root.setOnClickListener(view -> eventListener.onMessageClicked(messageResult));
|
||||
}
|
||||
|
||||
void bind(
|
||||
@NonNull QrInviteData inviteData,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener) {
|
||||
root.bind(inviteData, glideRequests);
|
||||
root.setOnClickListener(
|
||||
view -> eventListener.onInvitationClicked(inviteData.getRawQrString()));
|
||||
root.setOnLongClickListener(null);
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
root.unbind();
|
||||
root.setOnClickListener(null);
|
||||
root.setOnLongClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.b44t.messenger.DcChatlist;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcLot;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.search.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -18,13 +19,16 @@ class SearchViewModel extends ViewModel {
|
||||
private static final String TAG = "SearchViewModel";
|
||||
private final ObservingLiveData searchResult;
|
||||
private String lastQuery;
|
||||
private final Context appContext;
|
||||
private final DcContext dcContext;
|
||||
private boolean forwarding = false;
|
||||
private final Object bgSearchLock = new Object();
|
||||
private boolean inBgSearch;
|
||||
private boolean needsAnotherBgSearch;
|
||||
|
||||
SearchViewModel(@NonNull Context context) {
|
||||
this.dcContext = DcHelper.getContext(context.getApplicationContext());
|
||||
this.appContext = context.getApplicationContext();
|
||||
this.dcContext = DcHelper.getContext(appContext);
|
||||
this.searchResult = new ObservingLiveData();
|
||||
}
|
||||
|
||||
@@ -42,27 +46,29 @@ class SearchViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public void updateQuery() {
|
||||
if (inBgSearch) {
|
||||
needsAnotherBgSearch = true;
|
||||
Log.i(TAG, "... search call debounced");
|
||||
} else {
|
||||
synchronized (bgSearchLock) {
|
||||
if (inBgSearch) {
|
||||
Log.i(TAG, "... search call debounced");
|
||||
needsAnotherBgSearch = true;
|
||||
return;
|
||||
}
|
||||
inBgSearch = true;
|
||||
Util.runOnBackground(
|
||||
() -> {
|
||||
Util.sleep(100);
|
||||
needsAnotherBgSearch = false;
|
||||
queryAndCallback(lastQuery, searchResult::postValue);
|
||||
|
||||
while (needsAnotherBgSearch) {
|
||||
Util.sleep(100);
|
||||
needsAnotherBgSearch = false;
|
||||
Log.i(TAG, "... executing debounced search call");
|
||||
queryAndCallback(lastQuery, searchResult::postValue);
|
||||
}
|
||||
|
||||
inBgSearch = false;
|
||||
});
|
||||
}
|
||||
Util.runOnBackground(
|
||||
() -> {
|
||||
while (true) {
|
||||
Log.i(TAG, "... executing debounced search call");
|
||||
queryAndCallback(lastQuery, searchResult::postValue);
|
||||
synchronized (bgSearchLock) {
|
||||
if (!needsAnotherBgSearch) {
|
||||
inBgSearch = false;
|
||||
return;
|
||||
}
|
||||
needsAnotherBgSearch = false;
|
||||
}
|
||||
Util.sleep(100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void queryAndCallback(@NonNull String query, @NonNull SearchViewModel.Callback callback) {
|
||||
@@ -73,6 +79,12 @@ class SearchViewModel extends ViewModel {
|
||||
return;
|
||||
}
|
||||
|
||||
QrInviteData qrInviteData = null;
|
||||
if (!forwarding && query.contains(":")) {
|
||||
DcLot qrParsed = dcContext.checkQr(query);
|
||||
qrInviteData = QrInviteData.from(appContext, dcContext, qrParsed, query);
|
||||
}
|
||||
|
||||
// #1 search for chats
|
||||
long startMs = System.currentTimeMillis();
|
||||
DcChatlist conversations =
|
||||
@@ -83,7 +95,8 @@ class SearchViewModel extends ViewModel {
|
||||
// #2 search for contacts
|
||||
if (!query.equals(lastQuery) && overallCnt > 0) {
|
||||
Log.i(TAG, "... skipping getContacts() and searchMsgs(), more recent search pending");
|
||||
callback.onResult(new SearchResult(query, new int[0], conversations, new int[0]));
|
||||
callback.onResult(
|
||||
new SearchResult(query, new int[0], conversations, new int[0], qrInviteData));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,19 +108,19 @@ class SearchViewModel extends ViewModel {
|
||||
// #3 search for messages
|
||||
if (forwarding) {
|
||||
Log.i(TAG, "... searchMsgs() disabled by caller");
|
||||
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
|
||||
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.length() <= 1) {
|
||||
Log.i(TAG, "... skipping searchMsgs(), string too short");
|
||||
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
|
||||
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!query.equals(lastQuery) && overallCnt > 0) {
|
||||
Log.i(TAG, "... skipping searchMsgs(), more recent search pending");
|
||||
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
|
||||
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,7 +128,7 @@ class SearchViewModel extends ViewModel {
|
||||
int[] messages = dcContext.searchMsgs(0, query);
|
||||
Log.i(TAG, "⏰ searchMsgs(" + query + "): " + (System.currentTimeMillis() - startMs) + "ms");
|
||||
|
||||
callback.onResult(new SearchResult(query, contacts, conversations, messages));
|
||||
callback.onResult(new SearchResult(query, contacts, conversations, messages, qrInviteData));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.thoughtcrime.securesms.search.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.b44t.messenger.DcChatlist;
|
||||
import org.thoughtcrime.securesms.search.QrInviteData;
|
||||
|
||||
/**
|
||||
* Represents an all-encompassing search result that can contain various result for different
|
||||
@@ -16,16 +18,27 @@ public class SearchResult {
|
||||
private final int[] contacts;
|
||||
private final DcChatlist conversations;
|
||||
private final int[] messages;
|
||||
private final QrInviteData qrInviteData;
|
||||
|
||||
public SearchResult(
|
||||
@NonNull String query,
|
||||
@NonNull int[] contacts,
|
||||
@NonNull DcChatlist conversations,
|
||||
@NonNull int[] messages) {
|
||||
this(query, contacts, conversations, messages, null);
|
||||
}
|
||||
|
||||
public SearchResult(
|
||||
@NonNull String query,
|
||||
@NonNull int[] contacts,
|
||||
@NonNull DcChatlist conversations,
|
||||
@NonNull int[] messages,
|
||||
@Nullable QrInviteData qrInviteData) {
|
||||
this.query = query;
|
||||
this.contacts = contacts;
|
||||
this.conversations = conversations;
|
||||
this.messages = messages;
|
||||
this.qrInviteData = qrInviteData;
|
||||
}
|
||||
|
||||
public int[] getContacts() {
|
||||
@@ -44,8 +57,16 @@ public class SearchResult {
|
||||
return query;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public QrInviteData getQrInviteData() {
|
||||
return qrInviteData;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return contacts.length + conversations.getCnt() + messages.length;
|
||||
return (qrInviteData != null ? 1 : 0)
|
||||
+ contacts.length
|
||||
+ conversations.getCnt()
|
||||
+ messages.length;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
|
||||
@@ -57,22 +57,26 @@ public class LongClickCopySpan extends ClickableSpan {
|
||||
DcContext dcContext = DcHelper.getContext(activity);
|
||||
|
||||
int contactId = dcContext.lookupContactIdByAddr(addr);
|
||||
if (contactId == 0 && dcContext.mayBeValidAddr(addr)) {
|
||||
contactId = dcContext.createContact(null, addr);
|
||||
}
|
||||
DcContact contact = dcContext.getContact(contactId);
|
||||
if (contact.getId() != 0
|
||||
DcContact contact = (contactId != 0) ? dcContext.getContact(contactId) : null;
|
||||
if (contact != null
|
||||
&& !contact.isBlocked()
|
||||
&& dcContext.getChatIdByContactId(contact.getId()) != 0) {
|
||||
openChat(activity, contact);
|
||||
} else if (contact == null
|
||||
&& dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 1) {
|
||||
DcHelper.showInvalidUnencryptedDialog(activity);
|
||||
} else {
|
||||
String name = contact != null ? contact.getDisplayName() : addr;
|
||||
new AlertDialog.Builder(activity)
|
||||
.setMessage(
|
||||
activity.getString(R.string.ask_start_chat_with, contact.getDisplayName()))
|
||||
.setMessage(activity.getString(R.string.ask_start_chat_with, name))
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(dialog, which) -> {
|
||||
openChat(activity, contact);
|
||||
openChat(
|
||||
activity,
|
||||
contact == null
|
||||
? dcContext.getContact(dcContext.createContact(null, addr))
|
||||
: contact);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
@@ -151,7 +151,6 @@ public class SendRelayedMessageUtil {
|
||||
message = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
|
||||
} else if ("sticker".equals(type)) {
|
||||
message = new DcMsg(dcContext, DcMsg.DC_MSG_STICKER);
|
||||
message.forceSticker();
|
||||
} else if ("image".equals(type) || MediaUtil.isImageType(mimeType)) {
|
||||
message = new DcMsg(dcContext, DcMsg.DC_MSG_IMAGE);
|
||||
} else if ("audio".equals(type) || MediaUtil.isAudioType(mimeType)) {
|
||||
|
||||
@@ -15,7 +15,6 @@ public class ShareUtil {
|
||||
private static final String FORWARDED_MESSAGE_ACCID = "forwarded_message_accid";
|
||||
private static final String FORWARDED_MESSAGE_IDS = "forwarded_message_ids";
|
||||
private static final String SHARED_URIS = "shared_uris";
|
||||
private static final String SHARED_CONTACT_ID = "shared_contact_id";
|
||||
private static final String IS_SHARING = "is_sharing";
|
||||
private static final String IS_FROM_WEBXDC = "is_from_webxdc";
|
||||
private static final String SHARED_TITLE = "shared_title";
|
||||
@@ -69,7 +68,7 @@ public class ShareUtil {
|
||||
}
|
||||
}
|
||||
|
||||
static int[] getForwardedMessageIDs(Activity activity) {
|
||||
public static int[] getForwardedMessageIDs(Activity activity) {
|
||||
try {
|
||||
return activity.getIntent().getIntArrayExtra(FORWARDED_MESSAGE_IDS);
|
||||
} catch (NullPointerException npe) {
|
||||
@@ -112,15 +111,6 @@ public class ShareUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static int getSharedContactId(Activity activity) {
|
||||
try {
|
||||
return activity.getIntent().getIntExtra(SHARED_CONTACT_ID, 0);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getSharedText(Activity activity) {
|
||||
try {
|
||||
return activity.getIntent().getStringExtra(TEXT_EXTRA);
|
||||
@@ -142,7 +132,6 @@ public class ShareUtil {
|
||||
activity.getIntent().removeExtra(FORWARDED_MESSAGE_ACCID);
|
||||
activity.getIntent().removeExtra(FORWARDED_MESSAGE_IDS);
|
||||
activity.getIntent().removeExtra(SHARED_URIS);
|
||||
activity.getIntent().removeExtra(SHARED_CONTACT_ID);
|
||||
activity.getIntent().removeExtra(IS_SHARING);
|
||||
activity.getIntent().removeExtra(DIRECT_SHARING_CHAT_ID);
|
||||
activity.getIntent().removeExtra(TEXT_EXTRA);
|
||||
@@ -167,9 +156,6 @@ public class ShareUtil {
|
||||
if (!getSharedUris(currentActivity).isEmpty()) {
|
||||
newActivityIntent.putParcelableArrayListExtra(SHARED_URIS, getSharedUris(currentActivity));
|
||||
}
|
||||
if (getSharedContactId(currentActivity) != 0) {
|
||||
newActivityIntent.putExtra(SHARED_CONTACT_ID, getSharedContactId(currentActivity));
|
||||
}
|
||||
if (getSharedText(currentActivity) != null) {
|
||||
newActivityIntent.putExtra(TEXT_EXTRA, getSharedText(currentActivity));
|
||||
}
|
||||
@@ -220,16 +206,7 @@ public class ShareUtil {
|
||||
composeIntent.putExtra(IS_SHARING, true);
|
||||
}
|
||||
|
||||
public static void setSharedContactId(Intent composeIntent, int contactId) {
|
||||
composeIntent.putExtra(SHARED_CONTACT_ID, contactId);
|
||||
composeIntent.putExtra(IS_SHARING, true);
|
||||
}
|
||||
|
||||
public static void setSharedTitle(Intent composeIntent, String text) {
|
||||
composeIntent.putExtra(SHARED_TITLE, text);
|
||||
}
|
||||
|
||||
public static void setDirectSharing(Intent composeIntent, int chatId) {
|
||||
composeIntent.putExtra(DIRECT_SHARING_CHAT_ID, chatId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<objectAnimator
|
||||
android:propertyName="translationY"
|
||||
android:valueType="floatType"
|
||||
android:valueFrom="0dp"
|
||||
android:valueTo="-8dp"
|
||||
android:duration="1500"
|
||||
android:repeatMode="reverse"
|
||||
android:repeatCount="infinite"
|
||||
android:interpolator="@android:interpolator/accelerate_decelerate" />
|
||||
|
||||
</set>
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="228dp"
|
||||
android:height="280dp"
|
||||
android:viewportWidth="228"
|
||||
android:viewportHeight="280">
|
||||
<path
|
||||
android:pathData="m10.03,234.14c0.3,-0.01 0.6,-0.02 0.9,-0.04 -0.07,-0.49 -0.14,-0.97 -0.2,-1.46 -0.15,-1.34 -0.26,-2.69 -0.25,-4.04 -0.02,-0.86 -0.05,-1.71 -0.07,-2.57 -0.09,-0.06 -0.18,-0.13 -0.27,-0.19 -0.02,-0.02 -0.04,-0.03 -0.07,-0.05zM44.87,232.95c11.71,-8.35 26.86,-14.79 46.21,-15.9 0,0 39.93,-0.27 47.91,-3.53 7.98,-3.26 68.68,-14.69 82.94,-98.43 14.26,-83.74 -1.06,-115.09 -1.06,-115.09 0,0 -21.14,55.68 -81.02,59.81 0,0 -14.5,1.03 -38.82,1.42 -24.32,0.39 -75.77,20.65 -90.55,85.62l-0.22,43.44c2.5,4.22 5.49,8.12 8.91,11.66 3.99,4.11 8.11,8.12 12.79,11.45 2.26,1.65 4.65,3.2 6.51,5.33 1.94,2.34 3.33,5 4.2,7.93 0.71,2.1 1.45,4.2 2.2,6.28z"
|
||||
android:fillColor="#ffffff" />
|
||||
<path
|
||||
android:pathData="m217.97,45.86c-0.3,0.01 -0.6,0.02 -0.9,0.04 0.07,0.49 0.14,0.97 0.2,1.46 0.15,1.34 0.26,2.69 0.25,4.04 0.02,0.86 0.05,1.71 0.07,2.57 0.09,0.06 0.18,0.13 0.27,0.19 0.02,0.02 0.04,0.03 0.07,0.05zM183.13,47.05c-11.71,8.35 -26.86,14.79 -46.21,15.9 0,0 -39.93,0.27 -47.91,3.53 -7.98,3.26 -68.68,14.69 -82.94,98.43 -14.26,83.74 1.06,115.09 1.06,115.09 0,0 21.14,-55.68 81.02,-59.81 0,0 14.5,-1.03 38.82,-1.42 24.32,-0.39 75.77,-20.65 90.55,-85.62l0.22,-43.44c-2.5,-4.22 -5.49,-8.12 -8.91,-11.66 -3.99,-4.11 -8.11,-8.12 -12.79,-11.45 -2.26,-1.65 -4.65,-3.2 -6.51,-5.33 -1.94,-2.34 -3.33,-5 -4.2,-7.93 -0.71,-2.1 -1.45,-4.2 -2.2,-6.28z"
|
||||
android:fillColor="#ffffff" />
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="490.3dp"
|
||||
android:height="72.64dp"
|
||||
android:viewportWidth="490.3"
|
||||
android:viewportHeight="72.64">
|
||||
<path
|
||||
android:pathData="M17.45,58.55L50.75,58.55L49.76,46.94L18.53,46.94ZM33.92,30.83 L42.83,51.26 42.29,55.13 49.85,70.7L67.13,70.7L33.92,3.74 0.81,70.7L17.99,70.7L25.82,54.5 25.1,51.17ZM85.75,29.3L72.35,29.3v41.4h13.41zM97.72,43.34 L103.57,31.73q-1.08,-1.62 -3.15,-2.52 -2.07,-0.9 -4.32,-0.9 -3.42,0 -6.57,2.25 -3.15,2.25 -5.13,6.12 -1.89,3.87 -1.89,8.82l3.24,4.77q0,-2.97 0.81,-5.04 0.81,-2.07 2.34,-3.15 1.53,-1.08 3.51,-1.08 1.8,0 2.97,0.63 1.26,0.63 2.34,1.71zM119.41,50q0,-3.06 1.44,-5.49 1.44,-2.43 3.96,-3.87 2.61,-1.44 5.58,-1.44 2.25,0 4.59,0.72 2.34,0.72 4.32,2.07 2.07,1.35 3.33,3.33L142.63,32q-2.16,-1.53 -5.4,-2.52 -3.24,-1.08 -7.92,-1.08 -6.84,0 -12.33,2.7 -5.4,2.7 -8.55,7.56 -3.06,4.77 -3.06,11.34 0,6.48 3.06,11.34 3.15,4.86 8.55,7.56 5.49,2.7 12.33,2.7 4.68,0 7.92,-0.99 3.24,-1.08 5.4,-2.79L142.63,54.59q-1.26,1.98 -3.24,3.33 -1.89,1.35 -4.23,2.16 -2.25,0.72 -4.77,0.72 -2.97,0 -5.58,-1.35 -2.52,-1.35 -3.96,-3.69 -1.44,-2.43 -1.44,-5.76zM162.97,57.74q0,-1.53 0.72,-2.52 0.72,-1.08 2.16,-1.62 1.44,-0.54 3.87,-0.54 3.15,0 5.94,0.9 2.88,0.81 5.04,2.43v-6.03q-1.08,-1.17 -3.24,-2.25 -2.16,-1.08 -5.22,-1.8 -2.97,-0.72 -6.75,-0.72 -7.74,0 -11.97,3.51 -4.23,3.42 -4.23,9.36 0,4.23 1.98,7.2 2.07,2.97 5.4,4.5 3.42,1.44 7.38,1.44 3.96,0 7.38,-1.35 3.42,-1.44 5.58,-4.14 2.16,-2.79 2.16,-6.75l-1.44,-5.4q0,2.88 -1.26,4.86 -1.26,1.98 -3.24,2.97 -1.98,0.99 -4.32,0.99 -1.62,0 -2.97,-0.54 -1.35,-0.63 -2.16,-1.71 -0.81,-1.17 -0.81,-2.79zM156.94,41.99q0.9,-0.54 2.88,-1.35 1.98,-0.9 4.59,-1.53 2.7,-0.63 5.49,-0.63 1.89,0 3.33,0.36 1.53,0.36 2.52,1.17 0.99,0.81 1.44,1.98 0.54,1.08 0.54,2.61v26.1h13.05L190.77,41.54q0,-4.32 -2.52,-7.29 -2.52,-2.97 -6.84,-4.5 -4.32,-1.62 -9.81,-1.62 -5.85,0 -10.8,1.53 -4.95,1.53 -8.46,3.24zM227.85,45.5v25.2h14.13L241.98,44.06q0,-7.47 -3.51,-11.61 -3.51,-4.14 -11.25,-4.14 -4.59,0 -7.74,1.89 -3.06,1.89 -4.86,5.31L214.62,29.3L200.94,29.3v41.4h13.68L214.62,45.5q0,-2.43 0.9,-4.14 0.9,-1.71 2.52,-2.61 1.62,-0.9 3.78,-0.9 3.24,0 4.59,1.98 1.44,1.98 1.44,5.67zM273.74,71.6q7.92,0 13.41,-3.06 5.58,-3.06 8.82,-9.18l-12.15,-2.97q-1.53,2.7 -4.14,4.05 -2.52,1.35 -6.12,1.35 -3.15,0 -5.31,-1.35 -2.16,-1.44 -3.24,-4.14 -1.08,-2.7 -1.08,-6.48 0.09,-4.14 1.08,-6.84 1.08,-2.79 3.15,-4.14 2.07,-1.35 5.13,-1.35 2.43,0 4.23,1.08 1.8,1.08 2.79,3.06 0.99,1.98 0.99,4.68 0,0.63 -0.36,1.62 -0.27,0.9 -0.72,1.53l3.24,-4.14h-25.56v7.56h37.8q0.18,-0.63 0.18,-1.53 0,-0.99 0,-1.98 0,-6.66 -2.61,-11.34 -2.61,-4.68 -7.56,-7.11 -4.95,-2.52 -12.15,-2.52 -7.2,0 -12.51,2.61 -5.22,2.61 -8.01,7.47 -2.79,4.86 -2.79,11.52 0,6.57 2.88,11.43 2.88,4.86 8.1,7.56 5.31,2.61 12.51,2.61zM318.56,39.2q0,-5.85 2.61,-9.99 2.61,-4.14 6.84,-6.3 4.32,-2.16 9.45,-2.16 4.5,0 7.92,1.17 3.42,1.08 6.12,3.06 2.7,1.89 4.77,4.05L356.26,12.65q-3.78,-2.97 -8.28,-4.68 -4.5,-1.71 -11.43,-1.71 -7.56,0 -13.95,2.34 -6.39,2.34 -10.98,6.75 -4.59,4.41 -7.11,10.44 -2.52,6.03 -2.52,13.41 0,7.38 2.52,13.41 2.52,6.03 7.11,10.44 4.59,4.41 10.98,6.75 6.39,2.34 13.95,2.34 6.93,0 11.43,-1.71 4.5,-1.71 8.28,-4.68L356.26,49.37q-2.07,2.16 -4.77,4.05 -2.7,1.89 -6.12,3.06 -3.42,1.17 -7.92,1.17 -5.13,0 -9.45,-2.16 -4.23,-2.16 -6.84,-6.3 -2.61,-4.23 -2.61,-9.99zM381.01,0.5L367.33,0.5L367.33,70.7h13.68zM394.24,45.5v25.2h13.59L407.83,43.52q0,-5.13 -1.44,-8.64 -1.44,-3.51 -4.59,-5.31 -3.06,-1.8 -8.19,-1.8 -5.13,0 -8.46,2.43 -3.24,2.34 -4.86,6.39 -1.62,3.96 -1.62,8.91h2.34q0,-2.43 0.9,-4.05 0.9,-1.71 2.52,-2.61 1.62,-0.99 3.78,-0.99 3.33,0 4.68,1.98 1.35,1.98 1.35,5.67zM428.97,57.74q0,-1.53 0.72,-2.52 0.72,-1.08 2.16,-1.62 1.44,-0.54 3.87,-0.54 3.15,0 5.94,0.9 2.88,0.81 5.04,2.43v-6.03q-1.08,-1.17 -3.24,-2.25 -2.16,-1.08 -5.22,-1.8 -2.97,-0.72 -6.75,-0.72 -7.74,0 -11.97,3.51 -4.23,3.42 -4.23,9.36 0,4.23 1.98,7.2 2.07,2.97 5.4,4.5 3.42,1.44 7.38,1.44 3.96,0 7.38,-1.35 3.42,-1.44 5.58,-4.14 2.16,-2.79 2.16,-6.75l-1.44,-5.4q0,2.88 -1.26,4.86 -1.26,1.98 -3.24,2.97 -1.98,0.99 -4.32,0.99 -1.62,0 -2.97,-0.54 -1.35,-0.63 -2.16,-1.71 -0.81,-1.17 -0.81,-2.79zM422.94,41.99q0.9,-0.54 2.88,-1.35 1.98,-0.9 4.59,-1.53 2.7,-0.63 5.49,-0.63 1.89,0 3.33,0.36 1.53,0.36 2.52,1.17 0.99,0.81 1.44,1.98 0.54,1.08 0.54,2.61v26.1h13.05L456.78,41.54q0,-4.32 -2.52,-7.29 -2.52,-2.97 -6.84,-4.5 -4.32,-1.62 -9.81,-1.62 -5.85,0 -10.8,1.53 -4.95,1.53 -8.46,3.24zM462.09,29.3L462.09,40.55L489.8,40.55L489.8,29.3ZM469.38,14.9L469.38,70.7L482.43,70.7L482.43,14.9Z"
|
||||
android:strokeWidth="0.999939"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#ffffff" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<gradient
|
||||
android:startColor="#7b00c8"
|
||||
android:endColor="#5564da"
|
||||
android:angle="315" />
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?attr/input_panel_bg_color" />
|
||||
<corners android:radius="25dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/white" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<stroke
|
||||
android:width="2px"
|
||||
android:color="@color/white" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
@@ -24,7 +24,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="Contact photo"
|
||||
android:importantForAccessibility="no"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:layout_constraintWidth_percent="0.4"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
@@ -44,7 +44,7 @@
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="@color/calls_dark_surface"
|
||||
android:background="@android:color/black"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
@@ -61,7 +61,7 @@
|
||||
android:id="@+id/display_name_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/calls_dark_on_surface"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="John Doe" />
|
||||
@@ -70,7 +70,7 @@
|
||||
android:id="@+id/status_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/calls_dark_on_surface_variant"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
tools:text="Connecting..." />
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="Caller photo"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="@drawable/ic_person" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -141,7 +141,7 @@
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Audio"
|
||||
android:text="@string/audio"
|
||||
android:textSize="14sp"
|
||||
android:minWidth="100dp"
|
||||
app:icon="@drawable/ic_call"
|
||||
@@ -153,7 +153,7 @@
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Video"
|
||||
android:text="@string/video"
|
||||
android:textSize="14sp"
|
||||
android:minWidth="100dp"
|
||||
app:icon="@drawable/ic_videocam_on"
|
||||
@@ -185,7 +185,7 @@
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="Decline"
|
||||
android:text="@string/end_call"
|
||||
android:textSize="15sp"
|
||||
app:icon="@drawable/ic_call_end"
|
||||
app:iconGravity="textStart"
|
||||
@@ -199,7 +199,7 @@
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Answer"
|
||||
android:text="@string/answer_call"
|
||||
android:textSize="15sp"
|
||||
app:icon="@drawable/ic_call"
|
||||
app:iconGravity="textStart"
|
||||
@@ -241,9 +241,9 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_mic_on"
|
||||
android:contentDescription="Toggle microphone"
|
||||
app:tint="@color/calls_dark_on_surface"
|
||||
app:backgroundTint="@color/calls_dark_surface_3"
|
||||
android:contentDescription="@string/mute"
|
||||
app:tint="@color/calls_button_foreground"
|
||||
app:backgroundTint="@color/calls_button_background"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
@@ -263,9 +263,9 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_videocam_on"
|
||||
android:contentDescription="Toggle camera"
|
||||
app:tint="@color/calls_dark_on_surface"
|
||||
app:backgroundTint="@color/calls_dark_surface_3"
|
||||
android:contentDescription="@string/toggle_camera"
|
||||
app:tint="@color/calls_button_foreground"
|
||||
app:backgroundTint="@color/calls_button_background"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
@@ -285,8 +285,8 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_call_end"
|
||||
android:contentDescription="End call"
|
||||
app:tint="@android:color/white"
|
||||
android:contentDescription="@string/end_call2"
|
||||
app:tint="@color/calls_button_foreground"
|
||||
app:backgroundTint="@color/calls_red"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
@@ -307,9 +307,9 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_volume_up"
|
||||
android:contentDescription="Audio output"
|
||||
app:tint="@color/calls_dark_on_surface"
|
||||
app:backgroundTint="@color/calls_dark_surface_3"
|
||||
android:contentDescription="@string/audio"
|
||||
app:tint="@color/calls_button_foreground"
|
||||
app:backgroundTint="@color/calls_button_background"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
@@ -329,9 +329,9 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_switch_camera"
|
||||
android:contentDescription="Switch camera"
|
||||
app:tint="@color/calls_dark_on_surface"
|
||||
app:backgroundTint="@color/calls_dark_surface_3"
|
||||
android:contentDescription="@string/switch_camera"
|
||||
app:tint="@color/calls_button_foreground"
|
||||
app:backgroundTint="@color/calls_button_background"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:constraint_referenced_ids="inbox, imap_login, imap_server, imap_port, imap_security_label, imap_security, outbox_view_spacer_top,
|
||||
outbox, smtp_login, smtp_password, smtp_server, smtp_port, smtp_security_label, smtp_security, cert_check_label, cert_check, view_log_button" />
|
||||
outbox, smtp_login, smtp_password, smtp_server, smtp_port, smtp_security_label, smtp_security, cert_check_label, cert_check, view_log_button, enforce_e2ee_switch" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/advanced_icon"
|
||||
@@ -220,6 +220,23 @@
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/imap_folder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="58dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/imap_port">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/imap_folder_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:hint="IMAP folder" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/imap_security_label"
|
||||
style="@style/TextAppearance.AppCompat.Caption"
|
||||
@@ -228,7 +245,7 @@
|
||||
android:text="@string/login_imap_security"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/imap_port" />
|
||||
app:layout_constraintTop_toBottomOf="@id/imap_folder" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/imap_security"
|
||||
@@ -362,6 +379,17 @@
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/cert_check_label" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/enforce_e2ee_switch"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/enforce_e2ee"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/cert_check" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/view_log_button"
|
||||
android:layout_width="0dp"
|
||||
@@ -375,7 +403,7 @@
|
||||
android:paddingBottom="32dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/cert_check" />
|
||||
app:layout_constraintTop_toBottomOf="@id/enforce_e2ee_switch" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -36,20 +36,19 @@
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/attachment_editor_stub"
|
||||
android:inflatedId="@+id/attachment_editor"
|
||||
android:layout="@layout/conversation_activity_attachment_editor_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView
|
||||
android:id="@+id/conversation_activity_message_request_bottom_bar"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:background="@drawable/input_panel_rounded_bg"
|
||||
android:elevation="1dp"
|
||||
android:outlineProvider="background"
|
||||
android:visibility="gone"
|
||||
android:clickable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingRight="16dp"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user