mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc4ee3f686 | |||
| 18595cbee1 | |||
| dc78307df7 | |||
| 86a9cbfb45 | |||
| 623b20f713 | |||
| 5f4eae798f | |||
| 4a8609822d | |||
| 89ddc1e01f | |||
| 0ce42578fa | |||
| faa7ad0a35 | |||
| 38e8ceb253 | |||
| a32460f253 | |||
| 198268a4c3 | |||
| 71158970ae | |||
| 1383b06e86 | |||
| 24165e311b | |||
| 5ec892db34 | |||
| caef7eda29 | |||
| cf53af4778 | |||
| 415e0c2b5f | |||
| 9a22597473 | |||
| acb4eb2ae1 | |||
| 20c0354938 | |||
| eac112d602 | |||
| 9b4f659f67 | |||
| 0166d4e656 | |||
| 92b3761d2d | |||
| 484cee21c6 | |||
| 0e40318050 | |||
| b2cc76ff2e | |||
| 96acaaf000 | |||
| 400e5ea671 | |||
| 4fac460926 | |||
| 94a5631566 | |||
| ea91075107 | |||
| 0c7b82b9e4 | |||
| d765d3ddeb | |||
| 094fb1e2a4 | |||
| f7244c2152 | |||
| 2b5d1005e3 | |||
| b1eadf0716 | |||
| c001c13053 | |||
| ea4ec343bc | |||
| 5642e86f6a | |||
| 46db14fc3e | |||
| 0f6d9670ff | |||
| 4c3c24ae5a | |||
| eeb558d94d | |||
| 5e08f56dd3 | |||
| d99f150dd2 | |||
| 82b3100570 | |||
| 348b6fd3c1 | |||
| c2f492463f | |||
| 38239b2644 | |||
| 4b996c95de | |||
| 89d90efcef | |||
| d5b4bae502 | |||
| c3ec163e1a | |||
| a927a32909 | |||
| 9aab4517ef | |||
| 727e68edc7 | |||
| 9047de85c2 | |||
| 29c313ba58 | |||
| 487f601c09 | |||
| 7589b5ac37 | |||
| 8a9ce2ddd1 | |||
| be449f5afc | |||
| ac832a617e | |||
| 4fd2832370 | |||
| 52b072a7f3 | |||
| 55862757d6 | |||
| cbdf495c5f | |||
| a9d0d2e179 | |||
| d0cbf169dd | |||
| 833bc14405 | |||
| fb3620d0e3 | |||
| 02466e09fe | |||
| e39134faad | |||
| fa90b167ef | |||
| f5f20399ac | |||
| 659152dfcf | |||
| f34e87a593 | |||
| a172441155 | |||
| 90ca85ae9c | |||
| 40c0612412 | |||
| 5de79e3b0b | |||
| 39369aadd7 | |||
| b1e709d5bc | |||
| f760573fe4 | |||
| 4263ece65b | |||
| 217c45c102 | |||
| 3b2145ec79 | |||
| 566d1a1c13 | |||
| 09dab94807 | |||
| 6e3ddb8d96 | |||
| 8d7bb437eb | |||
| 392b43e8f7 | |||
| 32201a60c4 | |||
| bb114c570f | |||
| 42c52c10c7 | |||
| ade21a2cb8 | |||
| aced181d92 | |||
| 92cd1fc31c | |||
| 743138a100 | |||
| bebda06160 | |||
| f581930a40 | |||
| 70d4844dc6 | |||
| d3dd50dcac | |||
| ab8fdbad78 | |||
| 174bd9e986 | |||
| ab6f7ec97f | |||
| 6288463ddb | |||
| d700353ed8 | |||
| e979873bb3 | |||
| 331c77d317 | |||
| f9792615b7 | |||
| a94dc9336e | |||
| 6f1e11e860 | |||
| e720b4cef8 | |||
| 10f4534ee8 | |||
| 28c02a767d | |||
| 98f2ae5430 | |||
| 00c61133d5 | |||
| e457ab431d | |||
| b854fb6689 | |||
| 7c3a80296a | |||
| 4328bdd698 | |||
| e5f45df59a | |||
| 9dd270172b | |||
| 310f1fb921 | |||
| 5f14f0792f | |||
| fb3b43426a | |||
| 62b66d93ee | |||
| 6b12ff335d | |||
| ea522be8f4 | |||
| 44441f1ff9 | |||
| 3fca807356 | |||
| 4dc377233a | |||
| aabc60d36f | |||
| a653ac6382 | |||
| df7cc90da9 | |||
| eeb56a4630 | |||
| 713a87993e | |||
| 8ff55f3cf3 | |||
| 0fd72d2415 | |||
| 3c7f12c7b8 | |||
| 56abaa7c20 | |||
| 9335eedd04 | |||
| c13536031e | |||
| 57581634c4 | |||
| 4fa2535118 | |||
| 0ac0276dea | |||
| ed72a60f24 | |||
| 656aca7d1f | |||
| 3594847d8e | |||
| c17ac1d090 | |||
| 93ba86a779 | |||
| 738f5b2cc8 | |||
| 71a473d3e3 | |||
| d64d094de3 | |||
| 41df4eb03d | |||
| dde82ccb2f | |||
| c89f9ce875 | |||
| 87ac24124a | |||
| 138dcd7bdf | |||
| 0d51e097d5 | |||
| abff1c0c55 | |||
| 08643d3389 | |||
| 36249777e0 | |||
| b878ed4df3 | |||
| 16a02a5e4f | |||
| c614c5f004 | |||
| c7a542aac7 | |||
| e76f445985 | |||
| 69e18a009b | |||
| 64ef656544 |
@@ -0,0 +1,50 @@
|
||||
name: Core Cache
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'jni/deltachat-core-rust'
|
||||
- 'jni/Android.mk'
|
||||
- 'jni/Application.mk'
|
||||
- 'jni/dc_wrapper.c'
|
||||
- 'scripts/ndk-make.sh'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: android-actions/setup-android@v3
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: jni/deltachat-core-rust
|
||||
|
||||
- name: Get deltachat-core-rust submodule hash
|
||||
id: core-hash
|
||||
run: echo "hash=$(git rev-parse HEAD:jni/deltachat-core-rust)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache compiled core
|
||||
id: cache-core
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
jni/arm64-v8a
|
||||
libs/arm64-v8a
|
||||
key: core-arm64-v8a-${{ steps.core-hash.outputs.hash }}-${{ hashFiles('jni/Android.mk', 'jni/Application.mk', 'jni/dc_wrapper.c', 'scripts/ndk-make.sh') }}-ndk-r27
|
||||
|
||||
- name: Compile core
|
||||
if: steps.cache-core.outputs.cache-hit != 'true'
|
||||
env:
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
export PATH="${PATH}:${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/"
|
||||
scripts/install-toolchains.sh && scripts/ndk-make.sh arm64-v8a
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Code Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
spotless:
|
||||
name: Check code format (Spotless)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v4
|
||||
- name: Check formatting
|
||||
run: ./gradlew spotlessCheck
|
||||
@@ -14,16 +14,15 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Validate Fastlane Metadata
|
||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
working-directory: jni/deltachat-core-rust
|
||||
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'temurin'
|
||||
- uses: android-actions/setup-android@v3
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
@@ -32,15 +31,35 @@ jobs:
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v4
|
||||
|
||||
- uses: android-actions/setup-android@v3
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: jni/deltachat-core-rust
|
||||
|
||||
- name: Get deltachat-core-rust submodule hash
|
||||
id: core-hash
|
||||
run: echo "hash=$(git rev-parse HEAD:jni/deltachat-core-rust)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore compiled core
|
||||
id: core-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
jni/arm64-v8a
|
||||
libs/arm64-v8a
|
||||
key: core-arm64-v8a-${{ steps.core-hash.outputs.hash }}-${{ hashFiles('jni/Android.mk', 'jni/Application.mk', 'jni/dc_wrapper.c', 'scripts/ndk-make.sh') }}-ndk-r27
|
||||
|
||||
- name: Compile core
|
||||
if: steps.core-cache.outputs.cache-hit != 'true'
|
||||
env:
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
|
||||
@@ -72,14 +72,3 @@ jobs:
|
||||
files: |
|
||||
build/outputs/apk/foss/release/*.apk
|
||||
build/outputs/mapping/fossRelease/mapping-*.txt
|
||||
|
||||
- name: Release on ZapStore
|
||||
run: |
|
||||
export CHECKSUM=6e2c7cf6da53c3f1a78b523a6aacd6316dce3d74ace6f859c2676729ee439990
|
||||
curl -sL https://cdn.zapstore.dev/$CHECKSUM -o zapstore
|
||||
if echo "$CHECKSUM zapstore" | sha256sum -c --status; then
|
||||
chmod +x zapstore
|
||||
SIGN_WITH=${{ secrets.NOSTR_KEY }} ./zapstore publish --indexer-mode
|
||||
else
|
||||
echo "ERROR: checksum doesn't match!"
|
||||
fi
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ install Rust tooling (read sections below) and the [dcrpcgen tool](https://githu
|
||||
then generate the code running the script:
|
||||
|
||||
```
|
||||
./scripts/generate-rpc-bindings.sh
|
||||
./scripts/update-rpc-bindings.sh
|
||||
```
|
||||
|
||||
## Build Using Nix
|
||||
|
||||
+35
-1
@@ -2,15 +2,49 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
* Better incoming call system integration
|
||||
* Calls are not experimental anymore and don't need to be manually enabled
|
||||
|
||||
## v2.49.0
|
||||
2026-04
|
||||
|
||||
* Fix file sharing to certain apps (e.g. Material Files, etc.)
|
||||
* Fix problem with calls when microphone permission is not granted
|
||||
* Fix taking pictures and videos in devices with SD cards
|
||||
* Fix flipped orientation for some images
|
||||
* Fix: avoid empty contact request chats when using invite links in a multi-device setup
|
||||
* Remove proxy toggle from profile editing to avoid confusion
|
||||
* Updated translations
|
||||
* Update to core 2.49.0
|
||||
|
||||
## v2.48.0
|
||||
2026-03
|
||||
|
||||
* Add a warning when editing relays
|
||||
* Fix message reordering problems in multi-relay setups
|
||||
* Some more bug fixes and updated translations
|
||||
* Update to core 2.48.0
|
||||
|
||||
## v2.47.0
|
||||
2026-03
|
||||
|
||||
* Allow to set chat description
|
||||
* Unified date display in call bubbles
|
||||
* Explain at "Settings / Chats / Outgoing Media Quality" how to send original quality
|
||||
* Add a basic sticker picker
|
||||
* Leave groups and channels before deletion
|
||||
* Further minimize metadata in messages and while getting in contact.
|
||||
* Increase resilience of multi-relay usage: if on relay goes down, messages are still received in the others.
|
||||
* Allow to hide a relay from contacts instead of removing, allowing smoother relay changes
|
||||
* HTML emails: allow to review and copy links before opening them
|
||||
* Mark call message as seen when accepting/declining a call
|
||||
* Fix: keep original sent timestamp for resent messages
|
||||
* Fix: make clicking on broadcast member-added messages work always
|
||||
* Fix: remove notification when a message is deleted by sender
|
||||
* Update to core 2.44.0
|
||||
* Fix: avoid "reply privately" not quoting the selected message sometimes
|
||||
* Fix: properly hide the calls button
|
||||
* Some more bug fixes and updated translations
|
||||
* Update to core 2.47.0
|
||||
|
||||
## v2.43.0
|
||||
2026-02
|
||||
|
||||
+7
-10
@@ -73,18 +73,15 @@ esp. before/after screenshots.
|
||||
|
||||
### Coding Conventions
|
||||
|
||||
Source files are partly derived from different other open source projects
|
||||
and may follow different coding styles and conventions.
|
||||
|
||||
If you do a PR fixing a bug or adding a feature,
|
||||
please embrace the coding convention you see in the corresponding files,
|
||||
so that the result fits well together.
|
||||
|
||||
Do not refactor or rename things in the same PR
|
||||
to make the diff small and the PR easy to review.
|
||||
|
||||
Project language is Java.
|
||||
|
||||
Code formatting is enforced via [Spotless](https://github.com/diffplug/spotless) Gradle plugin.
|
||||
Auto-format all files by running `./gradlew spotlessApply` before opening a PR.
|
||||
CI will fail if files are not formatted correctly so make sure to run the formatter before pushing.
|
||||
|
||||
If you do a PR fixing a bug or adding a feature, do not refactor or rename things in the same PR
|
||||
to make the diff small and the PR easy to review.
|
||||
|
||||
By using [Delta Chat Core](https://github.com/deltachat/deltachat-core-rust)
|
||||
there is already a strong separation between "UI" and "Model".
|
||||
Further separations and abstraction layers are often not helpful
|
||||
|
||||
+63
-21
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.11.1'
|
||||
id 'com.google.gms.google-services' version '4.4.1'
|
||||
id 'com.diffplug.spotless' version '8.3.0'
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -33,8 +34,8 @@ android {
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
defaultConfig {
|
||||
versionCode 30000737
|
||||
versionName "2.43.0"
|
||||
versionCode 30000742
|
||||
versionName "2.49.0"
|
||||
|
||||
applicationId "chat.delta.lite"
|
||||
multiDexEnabled true
|
||||
@@ -72,10 +73,20 @@ android {
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
doNotStrip '**/*.so'
|
||||
keepDebugSymbols += ['*/mips/*.so', '*/mips64/*.so']
|
||||
keepDebugSymbols += [
|
||||
'*/mips/*.so',
|
||||
'*/mips64/*.so'
|
||||
]
|
||||
}
|
||||
resources {
|
||||
excludes += ['LICENSE.txt', 'LICENSE', 'NOTICE', 'asm-license.txt', 'META-INF/LICENSE', 'META-INF/NOTICE']
|
||||
excludes += [
|
||||
'LICENSE.txt',
|
||||
'LICENSE',
|
||||
'NOTICE',
|
||||
'asm-license.txt',
|
||||
'META-INF/LICENSE',
|
||||
'META-INF/NOTICE'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,14 +165,14 @@ android {
|
||||
}
|
||||
|
||||
if(!project.hasProperty("ABI_FILTER")) {
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
||||
universalApk true
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||
@@ -169,16 +180,16 @@ android {
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.all { output ->
|
||||
output.outputFileName = output.outputFileName
|
||||
.replace("android", "ArcaneChat")
|
||||
.replace("-release", "")
|
||||
.replace(".apk", "-${variant.versionName}.apk")
|
||||
.replace("android", "ArcaneChat")
|
||||
.replace("-release", "")
|
||||
.replace(".apk", "-${variant.versionName}.apk")
|
||||
if(project.hasProperty("ABI_FILTER")) {
|
||||
output.versionCodeOverride =
|
||||
variant.versionCode * 10 + project.ext.versionCodes.get(ABI_FILTER)
|
||||
output.versionCodeOverride =
|
||||
variant.versionCode * 10 + project.ext.versionCodes.get(ABI_FILTER)
|
||||
} else {
|
||||
output.versionCodeOverride =
|
||||
variant.versionCode * 10 + project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 4)
|
||||
}
|
||||
output.versionCodeOverride =
|
||||
variant.versionCode * 10 + project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +210,31 @@ android {
|
||||
renderScript true
|
||||
aidl true
|
||||
}
|
||||
}
|
||||
|
||||
spotless {
|
||||
java {
|
||||
target 'src/*/java/**/*.java'
|
||||
targetExclude 'src/main/java/chat/delta/**' // ignore auto-generated code
|
||||
googleJavaFormat() // Google style = 2-space indent, matches Android Studio defaults
|
||||
removeUnusedImports()
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
format 'xml', {
|
||||
target 'src/*/res/**/*.xml', 'src/*/AndroidManifest.xml'
|
||||
targetExclude 'src/*/res/values*/strings.xml' // line-break changes invalidate translations
|
||||
eclipseWtp('xml').configFile('spotless/eclipse-wtp-xml.prefs')
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
groovyGradle {
|
||||
target '*.gradle'
|
||||
greclipse()
|
||||
leadingTabsToSpaces(4)
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
|
||||
final def markwon_version = '4.6.2'
|
||||
@@ -214,6 +249,11 @@ dependencies {
|
||||
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
|
||||
|
||||
implementation 'androidx.concurrent:concurrent-futures:1.3.0'
|
||||
implementation 'androidx.core:core:1.17.0'
|
||||
implementation 'androidx.core:core-telecom:1.0.1'
|
||||
implementation 'im.conversations.webrtc:webrtc-android:129.0.0'
|
||||
// For Kotlin->Java interpolation
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.4'
|
||||
implementation 'androidx.sharetarget:sharetarget:1.2.0'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
@@ -251,7 +291,8 @@ dependencies {
|
||||
implementation 'com.github.amulyakhare:TextDrawable:558677ea31' // number of unread messages,
|
||||
// the one-letter circle for the contacts (when there is not avatar) and a white background.
|
||||
implementation 'com.googlecode.mp4parser:isoparser:1.0.6' // MP4 recoding; upgrading eg. to 1.1.22 breaks recoding, however, i have not investigated further, just reset to 1.0.6
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.10.0') { // for the zooming on photos / media
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.10.0') {
|
||||
// for the zooming on photos / media
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
|
||||
@@ -260,7 +301,8 @@ dependencies {
|
||||
// <https://github.com/cketti/SafeContentResolver>
|
||||
implementation 'de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0'
|
||||
|
||||
gplayImplementation('com.google.firebase:firebase-messaging:24.1.2') { // for PUSH notifications, don't upgrade: v25.0.0 requires minSdk>=23
|
||||
gplayImplementation('com.google.firebase:firebase-messaging:24.1.2') {
|
||||
// for PUSH notifications, don't upgrade: v25.0.0 requires minSdk>=23
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
ArcaneChat is a decentralized and secure instant messenger that is easy to use for friends and family.
|
||||
|
||||
• Anonymous. Instant onboarding without a phone number, e-mail or other private data.
|
||||
• Private. Instant on-boarding without a phone number or other private data.
|
||||
|
||||
• Friends & Family: Only chat with people you know. Unknown users cannot message or follow you without mutual consent.
|
||||
|
||||
• Flexible. Supports multiple chat profiles and is easy to setup on multiple devices.
|
||||
|
||||
|
||||
Generated
+15
-15
@@ -7,11 +7,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756239746,
|
||||
"narHash": "sha256-0ibN685tT+u/Nbmbrrq9G3mRUzct2Votyv/a7Wwv26s=",
|
||||
"lastModified": 1775939535,
|
||||
"narHash": "sha256-Zq1U7Vhw5ctvhJQy+3ShqIv7Mplf3XkgJY+QJnhfUGQ=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "256631d162ec883b2341ee59621516e1f65f0f6b",
|
||||
"rev": "d2e16192309bf3101e4998ffa62e914819dee077",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -28,11 +28,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1741473158,
|
||||
"narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=",
|
||||
"lastModified": 1768818222,
|
||||
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0",
|
||||
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -79,11 +79,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1756125398,
|
||||
"narHash": "sha256-XexyKZpf46cMiO5Vbj+dWSAXOnr285GHsMch8FBoHbc=",
|
||||
"lastModified": 1775710090,
|
||||
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3b9f00d7a7bf68acd4c4abb9d43695afb04e03a5",
|
||||
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -95,11 +95,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1756159630,
|
||||
"narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=",
|
||||
"lastModified": 1775823930,
|
||||
"narHash": "sha256-ALT447J7FcxP/97J01A/gp/hgdO5lXRsm+zLMt+gIjc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b",
|
||||
"rev": "8c11f88bb9573a10a7d6bf87161ef08455ac70b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -138,11 +138,11 @@
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1763347184,
|
||||
"narHash": "sha256-6QH8hpCYJxifvyHEYg+Da0BotUn03BwLIvYo3JAxuqQ=",
|
||||
"lastModified": 1775877051,
|
||||
"narHash": "sha256-wpSQm2PD/w4uRo2wb8utk0b5hOBkkg/CZ1xICY+qB7M=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "08895cce80433978d5bfd668efa41c5e24578cbd",
|
||||
"rev": "08b4f3633471874c8894632ade1b78d75dbda002",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -946,18 +946,6 @@ JNIEXPORT jstring Java_com_b44t_messenger_DcContext_getContactEncrInfo(JNIEnv *e
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jstring Java_com_b44t_messenger_DcContext_initiateKeyTransfer(JNIEnv *env, jobject obj)
|
||||
{
|
||||
jstring setup_code = NULL;
|
||||
char* temp = dc_initiate_key_transfer(get_dc_context(env, obj));
|
||||
if (temp) {
|
||||
setup_code = JSTRING_NEW(temp);
|
||||
dc_str_unref(temp);
|
||||
}
|
||||
return setup_code;
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT void Java_com_b44t_messenger_DcContext_imex(JNIEnv *env, jobject obj, jint what, jstring dir)
|
||||
{
|
||||
CHAR_REF(dir);
|
||||
@@ -1630,15 +1618,6 @@ JNIEXPORT jboolean Java_com_b44t_messenger_DcMsg_hasHtml(JNIEnv *env, jobject ob
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getSetupCodeBegin(JNIEnv *env, jobject obj)
|
||||
{
|
||||
char* temp = dc_msg_get_setupcodebegin(get_dc_msg(env, obj));
|
||||
jstring ret = JSTRING_NEW(temp);
|
||||
dc_str_unref(temp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT void Java_com_b44t_messenger_DcMsg_setSubject(JNIEnv *env, jobject obj, jstring text)
|
||||
{
|
||||
CHAR_REF(text);
|
||||
|
||||
+1
-1
Submodule jni/deltachat-core-rust updated: 27dbe0bb6c...570119830f
Vendored
+5
@@ -13,3 +13,8 @@
|
||||
-keep class org.thoughtcrime.securesms.crypto.KeyStoreHelper* { *; }
|
||||
|
||||
-dontwarn com.google.firebase.analytics.connector.AnalyticsConnector
|
||||
|
||||
# Keep WebRTC classes
|
||||
-keep class org.webrtc.** { *; }
|
||||
-keepclassmembers class org.webrtc.** { *; }
|
||||
-keepattributes InnerClasses
|
||||
|
||||
@@ -9,3 +9,4 @@ cd "$ROOT_DIR"
|
||||
|
||||
# generate code
|
||||
dcrpcgen java --schema schema.json -o ./src/main/java/
|
||||
rm schema.json
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
eclipse.preferences.version=1
|
||||
lineWidth=100
|
||||
splitMultiAttrs=true
|
||||
indentMultipleAttributes=true
|
||||
indentationChar=space
|
||||
indentationSize=4
|
||||
spaceBeforeEmptyCloseTag=true
|
||||
@@ -3,10 +3,6 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- force compiling emojipicker on sdk<21 and firebase on sdk<19; runtime checks are required then -->
|
||||
<uses-sdk
|
||||
tools:overrideLibrary="androidx.emoji2.emojipicker, com.google.firebase.messaging, com.google.android.gms.cloudmessaging" />
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_deactivated"
|
||||
android:value="true" />
|
||||
|
||||
+548
-455
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@
|
||||
<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="#podporuje-delta-chat-obrázky-videa-a-jiné-přílohy">Podporuje Delta Chat obrázky, videa a jiné přílohy?</a></li>
|
||||
<li><a href="#multiple-accounts">What are profiles? How can I switch between them?</a></li>
|
||||
<li><a href="#kdo-uvidí-můj-profilový-obrázek">Kdo uvidí můj profilový obrázek?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#what-does-the-green-dot-mean">What does the green dot mean?</a></li>
|
||||
<li><a href="#what-do-the-ticks-shown-beside-outgoing-messages-mean">What do the ticks shown beside outgoing messages mean?</a></li>
|
||||
<li><a href="#edit">Correct typos and delete messages after sending</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">How do disappearing messages work?</a></li>
|
||||
<li><a href="#delold">What happens if I turn on “Delete Messages from Device”?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
@@ -218,19 +218,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="podporuje-delta-chat-obrázky-videa-a-jiné-přílohy">
|
||||
|
||||
|
||||
Podporuje Delta Chat obrázky, videa a jiné přílohy? <a href="#podporuje-delta-chat-obrázky-videa-a-jiné-přílohy" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes. 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>For performance, images are optimized and sent at a smaller size by default, but you can send it as a “file” to preserve the original.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -412,6 +399,32 @@ Notifications are not sent and there is no time limit.</p>
|
||||
<p>Note, that the original message may still be received by chat members
|
||||
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1030,22 +1043,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1561,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/cs/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<li><a href="#howtoe2ee">Wie finde ich Leute, mit denen ich chatten kann?</a></li>
|
||||
<li><a href="#warum-ist-ein-chat-als-anfrage-markiert">Warum ist ein Chat als “Anfrage” markiert?</a></li>
|
||||
<li><a href="#wie-kann-ich-zwei-meiner-freunde-miteinander-in-kontakt-bringen">Wie kann ich zwei meiner Freunde miteinander in Kontakt bringen?</a></li>
|
||||
<li><a href="#unterstützt-delta-chat-bilder-videos-und-dateianhänge">Unterstützt Delta Chat Bilder, Videos und Dateianhänge?</a></li>
|
||||
<li><a href="#multiple-accounts">Was sind Profile? Wie kann ich zwischen ihnen wechseln?</a></li>
|
||||
<li><a href="#wer-sieht-mein-profilbild">Wer sieht mein Profilbild?</a></li>
|
||||
<li><a href="#signature">Kann ich einen Status festlegen?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#was-bedeutet-der-grüne-punkt">Was bedeutet der grüne Punkt?</a></li>
|
||||
<li><a href="#was-bedeuten-die-häkchen-neben-den-ausgehenden-nachrichten">Was bedeuten die Häkchen neben den ausgehenden Nachrichten?</a></li>
|
||||
<li><a href="#edit">Schreibfehler korrigieren und Nachrichten nach dem Senden löschen</a></li>
|
||||
<li><a href="#mediaquality">Medienqualität und Datenverbrauch</a></li>
|
||||
<li><a href="#ephemeralmsgs">Wie funktionieren “Verschwindende Nachrichten”?</a></li>
|
||||
<li><a href="#delold">Was passiert, wenn ich “Nachrichten vom Gerät löschen” aktiviere?</a></li>
|
||||
<li><a href="#remove-account">Wie kann ich mein Chat-Profil löschen?</a></li>
|
||||
@@ -109,7 +109,7 @@
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Einfache Erstellung von <strong>privaten Chat-Profile</strong> mit sicheren, schnellen und interoperablen <a href="https://chatmail.at/relays">Chatmail-Servern</a>,
|
||||
<p>Einfache Erstellung von <strong>privaten Chat-Profilen</strong> mit sicheren, schnellen und interoperablen <a href="https://chatmail.at/relays">Chatmail-Servern</a>,
|
||||
die sofortige Push-Benachrichtigungen für iOS- und Android-Geräte bieten.</p>
|
||||
</li>
|
||||
<li>
|
||||
@@ -137,17 +137,17 @@ basierend auf <a href="https://github.com/chatmail/core/blob/main/standards.md#s
|
||||
</h3>
|
||||
|
||||
<p>Beachte zunächst, dass Delta Chat ein privater Messenger ist.
|
||||
Es gibt keine öffentliches Verzeichnis, du entscheiden selbst über deine Kontakte.</p>
|
||||
Es gibt kein öffentliches Verzeichnis, du entscheidest selbst über deine Kontakte.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Wenn du <strong>persönlich</strong> mit deinen Freunden oder Familie zusammen bist,
|
||||
tippe auf das <strong>QR-Code</strong>-Symbol <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" />
|
||||
auf dem Hauptbildschirm.<br />
|
||||
Bitte deinen Chatpartner den QR-Code mit Delta Chat zu <strong>scannen</strong>.</p>
|
||||
Bitte dann deinen Chatpartner den QR-Code mit Delta Chat zu <strong>scannen</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Für eine Kontaktaufnahme <strong>aus der Ferne</strong>, klicke im selben Bildschirm auf “Kopieren” oder “Teilen” und sende den <strong>Einladungslink</strong> über einen anderen privaten Chat.</p>
|
||||
<p>Für eine Kontaktaufnahme <strong>aus der Ferne</strong> klicke im selben Bildschirm auf “Kopieren” oder “Teilen” und sende den <strong>Einladungslink</strong> über einen anderen privaten Chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -177,7 +177,7 @@ wird eine Ende-zu-Ende-Verschlüsselung zwischen allen Mitgliedern eingerichtet.
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Da Delta Chat ein privater Messenger ist, können dir zunächst nur Freunde und Familienmitglieder, denen du deinen <a href="#howtoe2ee">QR-Code oder Einladungslink</a> schickst, schreiben.</p>
|
||||
<p>Da Delta Chat ein privater Messenger ist, können dir zunächst nur Freunde und Familienmitglieder schreiben, denen du deinen <a href="#howtoe2ee">QR-Code oder Einladungslink</a> schickst.</p>
|
||||
|
||||
<p>Deine Freunde können deine Kontaktdaten dann mit anderen Freunden teilen. Dies wird als <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Anfrage</b> angezeigt.</p>
|
||||
|
||||
@@ -186,12 +186,12 @@ wird eine Ende-zu-Ende-Verschlüsselung zwischen allen Mitgliedern eingerichtet.
|
||||
<p>Du musst die Anfrage <strong>akzeptieren</strong>, bevor du antworten kannst.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Du kannst sie auch “löschen”, wenn du vorerst nicht mit ihm chatten möchten.</p>
|
||||
<p>Du kannst sie auch “löschen”, wenn du vorerst nicht mit dieser Person chatten möchtest.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you delete a request, future messages from that contact will still appear
|
||||
as message request, so you can change your mind. If you really don’t want to
|
||||
receive messages from this person, consider <strong>blocking</strong> them.</p>
|
||||
<p>Wenn du eine Anfrage löschst, werden zukünftige Nachrichten von diesem Kontakt weiterhin
|
||||
als Nachrichtenanfrage angezeigt, sodass du deine Meinung ändern kannst. Wenn du wirklich keine
|
||||
Nachrichten von dieser Person erhalten möchtest, solltest du sie <strong>blockieren</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -209,19 +209,6 @@ Du kannst auch eine kurze Nachricht hinzufügen.</p>
|
||||
<p>Der zweite Kontakt erhält dann die <strong>Kontaktdaten</strong> und
|
||||
kann darauf tippen, um mit dem ersten Kontakt zu chatten.</p>
|
||||
|
||||
<h3 id="unterstützt-delta-chat-bilder-videos-und-dateianhänge">
|
||||
|
||||
|
||||
Unterstützt Delta Chat Bilder, Videos und Dateianhänge? <a href="#unterstützt-delta-chat-bilder-videos-und-dateianhänge" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Ja. Bilder, Videos, Dateien, Sprachnachrichten und mehr können über die <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Anhang-</strong>
|
||||
bzw. <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Sprachnachricht</strong>-Buttons hinzugefügt werden</p>
|
||||
|
||||
<p>Um die Leistung zu verbessern, werden die Bilder standardmäßig optimiert und in einer kleineren Größe gesendet, aber du kannst sie auch als “Datei” senden, um das Original zu erhalten.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -241,7 +228,7 @@ oder <strong>Profile zu wechseln</strong>.</p>
|
||||
|
||||
<p>Du kannst separate Profile für politische, familiäre oder berufliche Aktivitäten verwenden.</p>
|
||||
|
||||
<p>Vielleicht möchtest due auch erfahren, wie du <a href="#multiclient">Profile auf mehreren Geräten verwenden kannst</a>.</p>
|
||||
<p>Vielleicht möchtest du auch erfahren, wie du <a href="#multiclient">Profile auf mehreren Geräten verwenden kannst</a>.</p>
|
||||
|
||||
<h3 id="wer-sieht-mein-profilbild">
|
||||
|
||||
@@ -279,7 +266,7 @@ Sobald du eine Nachricht an einen Kontakt sendest, kann dieser deine Signatur in
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong>Angeheftete Chats</strong> bleiben immer ganz oben in der Chatliste. So kannst du schnell auf deine Lieblingschats zugreifen oder du verwendest vorübergehend angeheftete Chats um Dinge nicht zu vergessen.</p>
|
||||
<p><strong>Angeheftete Chats</strong> bleiben immer ganz oben in der Chatliste. So kannst du schnell auf deine Lieblingschats zugreifen oder du verwendest vorübergehend angeheftete Chats, um Dinge nicht zu vergessen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Stummgeschaltete Chats</strong> erhalten keine Benachrichtigungen, bleiben ansonsten aber an ihrem Platz. Du kannst auch stummgeschaltete Chats anheften.</p>
|
||||
@@ -292,7 +279,7 @@ Sobald du eine Nachricht an einen Kontakt sendest, kann dieser deine Signatur in
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Um die Funktionen zu nutzen, lang auf einen Chat in der Chatliste tippen oder den Chat mit der rechten Maustaste anklicken.</p>
|
||||
<p>Um die Funktionen zu nutzen, tippe lang auf einen Chat in der Chatliste oder klicke den Chat mit der rechten Maustaste an.</p>
|
||||
|
||||
<h3 id="save">
|
||||
|
||||
@@ -302,11 +289,11 @@ Sobald du eine Nachricht an einen Kontakt sendest, kann dieser deine Signatur in
|
||||
|
||||
</h3>
|
||||
|
||||
<p><strong>Gespeicherte Nachrichten</strong> ist ein Chat, den du verwenden kannst, um dir Nachrichten zu merken und wiederzufinden.</p>
|
||||
<p><strong>Gespeicherte Nachrichten</strong> ist ein Chat, den du verwenden kannst, um dir Nachrichten zu merken und sie wiederzufinden.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Tippen in einem beliebigen Chat lange auf eine Nachricht oder klicken mit der rechten Maustaste darauf und wähle <strong>Speichern</strong>.</p>
|
||||
<p>Tippe im Chat lange auf eine Nachricht oder klicke mit der rechten Maustaste darauf und wähle <strong>Speichern</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Gespeicherte Nachrichten werden mit dem Symbol
|
||||
@@ -319,7 +306,7 @@ Durch Tippen auf <img style="vertical-align:middle; width:1.2em; margin:1px" src
|
||||
kannst du zu der ursprünglichen Nachricht im ursprünglichen Chat zurückkehren</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Schließlich kannst du auch „Gespeicherte Nachrichten“ verwenden, um <strong>persönliche Notizen</strong> zu machen - öffnen den Chat, gib etwas ein, fügen ein Foto oder eine Sprachnachricht hinzu usw.</p>
|
||||
<p>Schließlich kannst du auch „Gespeicherte Nachrichten“ verwenden, um <strong>persönliche Notizen</strong> zu machen. Öffne den Chat, gib etwas ein, fügen ein Foto oder eine Sprachnachricht hinzu usw.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Da „Gespeicherte Nachrichten“ synchronisiert werden, können sie sehr praktisch für die Übertragung von Daten zwischen Geräten sein</p>
|
||||
@@ -360,7 +347,7 @@ sei es durch den <a href="#edit">Absender</a>, durch <a href="#delold">Automatis
|
||||
|
||||
<p>In <a href="#groups">Gruppen</a> bedeutet das zweite Häkchen, dass die Nachricht von mindestens einem Mitglied gelesen wurde.</p>
|
||||
|
||||
<p>Du erhälst nur dann das zweite Häkchen, wenn sowohl du als auch einer der Empfänger, die die Nachricht gelesen haben, <strong>Einstellungen → Chats → Lesebestätigungen</strong> aktiviert haben.</p>
|
||||
<p>Du erhältst nur dann das zweite Häkchen, wenn sowohl du als auch einer der Empfänger, die die Nachricht gelesen haben, <strong>Einstellungen → Chats → Lesebestätigungen</strong> aktiviert haben.</p>
|
||||
|
||||
<h3 id="edit">
|
||||
|
||||
@@ -388,6 +375,31 @@ Es werden keine Benachrichtigungen verschickt und es gibt kein Zeitlimit.</p>
|
||||
<p>Beachten, dass die ursprüngliche Nachricht dennoch von Chatteilnehmern empfangen worden sein könnte,
|
||||
die die Nachricht bereits beantwortet, weitergeleitet, gespeichert, mit einem Screenshot versehen oder anderweitig kopiert haben könnten.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
Medienqualität und Datenverbrauch <a href="#mediaquality" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Bilder, Videos, Dateien, Sprachnachrichten und mehr können über die <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Anhang-</strong>
|
||||
bzw. <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Sprachnachricht</strong>-Buttons hinzugefügt werden.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Standardmäßig sorgt Komprimierung für eine <strong>schnelle, effiziente Übertragung</strong>, die die Datenlimits und Speicherplatzkapazitäten aller Beteiligten berücksichtigt.
|
||||
Dies ist ideal für die tägliche Kommunikation.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>In Regionen mit schlechter Verbindung können Sie unter <strong>Einstellungen → Chats → Medienqualität beim Senden</strong> eine höhere Komprimierung wählen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Wenn Sie Medien ausdrücklich in ihrer <strong>Originalqualität</strong> senden müssen, verwende im Chat die Option <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Anhängen → Datei</strong>.
|
||||
Verwende diese Methode nur sparsam, da das Senden von Originaldateien den Datenverbrauch für dich und alle Empfänger im Chat erheblich erhöht.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -503,7 +515,7 @@ und seine <a href="#edit">eigenen Nachrichten von Geräten der Mitglieder lösch
|
||||
<p>Wenn das Mitglied noch nicht in deiner Kontaktliste ist, sie sich aber <strong>persönlich</strong> treffen, wählen Sie dort <strong>QR-Einladungscode</strong> an. Dein Chat-Partner kann nun den QR-Code mit seiner Delta Chat-App <strong>scannen</strong> indem er auf <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> auf dem Hauptbildschirm tippt.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Für eine Kontaktaufnahme <strong>aus der Ferne</strong>, tippe dort “Kopieren” oder “Teilen” und sende den Einladungslink über einen anderen privaten Chat zum neuen Mitglied.</p>
|
||||
<p>Für eine Kontaktaufnahme <strong>aus der Ferne</strong> tippe dort “Kopieren” oder “Teilen” und sende den Einladungslink über einen anderen privaten Chat zum neuen Mitglied.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -533,7 +545,7 @@ Kein Problem, bitte einfach ein anderes Gruppenmitglied in einem normalen Chat,
|
||||
Wenn du der Gruppe später erneut beitreten möchtest, bitten ein anderes Gruppenmitglied, dich hinzuzufügen.</li>
|
||||
</ul>
|
||||
|
||||
<p>Alternativ kannst du eine Gruppe auch “stummschalten” - dies bedeutet, dass du weiterhin alle Nachrichten erhälst und neue schreiben kannst, aber nicht mehr über neue Nachrichten informiert wirst.</p>
|
||||
<p>Alternativ kannst du eine Gruppe auch “stummschalten” - dies bedeutet, dass du weiterhin alle Nachrichten erhältst und neue schreiben kannst, aber nicht mehr über neue Nachrichten informiert wirst.</p>
|
||||
|
||||
<h3 id="eine-gruppe-klonen">
|
||||
|
||||
@@ -958,18 +970,16 @@ kannst du jedoch unter <strong>Einstellungen → Erweitert → Relays</strong>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Du kannst ein Relay <strong>hinzufügen</strong>, indem du einen QR-Code scannst,
|
||||
z.B. von <a href="https://chatmail.at/relays">https://chatmail.at/relays</a>.
|
||||
Bei mehreren Relays, empfängst du die Nachrichten von allen Relays.</p>
|
||||
<p>Du kannst ein Relay <strong>hinzufügen</strong>, indem du einen QR-Code scannst, z.B. von <a href="https://chatmail.at/relays">chatmail.at/relays</a>. Bei mehreren Relays, empfängst du die Nachrichten von allen Relays. Deine Kontakte lernen deine Relays automatisch, sobald du ihnen schreibst.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Standard</strong> legt das Relay fest, an das deine Chatpartner zukünftig Nachrichten senden.</p>
|
||||
<p>Tippe ein Relay an, um es <strong>Zum Senden zu verwenden</strong></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Wenn du ein Relay <strong>entfernst</strong>,
|
||||
stelle sicher, dass ein anderes Standard-Relay ausreichend lange verwendet wurde.
|
||||
Andernfalls erreichen dich keine Nachrichten von deinen Kontakten.
|
||||
Im Zweifelsfall entferne das Relay später.</p>
|
||||
<p>Wenn du ein Relay <strong>entfernst</strong>, können Kontakte, die nur dieses Relay kennen, dich nicht erreichen, bis du ihnen wieder schreibst. Um erreichbar zu bleiben, wähle <strong>Vor Kontakten verstecken</strong> im Bestätigungsdialog anstelle das Relay direkt zu löschen.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Um ein verstecktes Relay wieder <strong>anzuzeigen</strong> tippe es an.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1458,7 +1468,7 @@ Weitere Informationen findest du in unserem Blogbeitrag über <a href="https://d
|
||||
<p>Im April 2023 haben wir Sicherheits- und Datenschutzprobleme mit dem “In Chats geteilten Apps”-Feature behoben, die mit Fehlern beim Sandboxing, insbesondere mit Chromium zusammenhängen. Wir haben daraufhin eine unabhängige Sicherheitsprüfung von Cure53 durchführen lassen, und alle gefundenen Probleme wurden mit den im April 2023 veröffentlichten 1.36 Releases behoben. Siehe <a href="https://delta.chat/en/2023-05-22-webxdc-security">hier für die vollständige Hintergrundgeschichte</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Im März 2023 analysierte <a href="https://cure53.de">Cure53</a> sowohl die Transportverschlüsselung von Delta Chats Netzwerkverbindungen als auch das reproduzierbare Mailserver-Setup wie <a href="https://delta.chat/de/serverguide">auf dieser Seite empfohlen</a>. Du kannst mehr über das Audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">in unserem Blog</a> lesen oder du liest den <a href="https://delta.chat/assets/blog/MER-01-report.pdf">vollständigen Bericht hier</a>.</p>
|
||||
<p>Im März 2023 analysierte <a href="https://cure53.de">Cure53</a> sowohl die Transportverschlüsselung von Delta Chats Netzwerkverbindungen als auch das reproduzierbare Mailserver-Setup wie <a href="https://delta.chat/serverguide">auf dieser Seite empfohlen</a>. Du kannst mehr über das Audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">in unserem Blog</a> lesen oder du liest den <a href="https://delta.chat/assets/blog/MER-01-report.pdf">vollständigen Bericht hier</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Im Jahr 2020 analysierte <a href="https://includesecurity.com">Include Security</a> Delta Chats Rust <a href="https://github.com/deltachat/deltachat-core-rust/">core</a>, <a href="https://github.com/async-email/async-imap">IMAP</a>,<a href="https://github.com/async-email/async-smtp">SMTP</a>, und <a href="https://github.com/async-email/async-native-tls">TLS</a> Bibliotheken.
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="#does-delta-chat-support-images-videos-and-other-attachments">Does Delta Chat support images, videos and other attachments?</a></li>
|
||||
<li><a href="#multiple-accounts">What are profiles? How can I switch between them?</a></li>
|
||||
<li><a href="#who-sees-my-profile-picture">Who sees my profile picture?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#what-does-the-green-dot-mean">What does the green dot mean?</a></li>
|
||||
<li><a href="#what-do-the-ticks-shown-beside-outgoing-messages-mean">What do the ticks shown beside outgoing messages mean?</a></li>
|
||||
<li><a href="#edit">Correct typos and delete messages after sending</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">How do disappearing messages work?</a></li>
|
||||
<li><a href="#delold">What happens if I turn on “Delete Messages from Device”?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
@@ -218,19 +218,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="does-delta-chat-support-images-videos-and-other-attachments">
|
||||
|
||||
|
||||
Does Delta Chat support images, videos and other attachments? <a href="#does-delta-chat-support-images-videos-and-other-attachments" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes. 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>For performance, images are optimized and sent at a smaller size by default, but you can send it as a “file” to preserve the original.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -411,6 +398,32 @@ Notifications are not sent and there is no time limit.</p>
|
||||
<p>Note, that the original message may still be received by chat members
|
||||
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1030,22 +1043,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1561,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/en/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="#delta-chat-soporta-envío-de-imágenes-videos-documentos-y-otros-archivos">¿Delta Chat soporta envío de imágenes, videos, documentos y otros archivos?</a></li>
|
||||
<li><a href="#multiple-accounts">¿Qué son los perfiles? ¿Cómo puedo cambiar entre ellos?</a></li>
|
||||
<li><a href="#quién-ve-mi-foto-de-perfil">¿Quién ve mi foto de perfil?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#qué-significa-el-punto-verde">¿Qué significa el punto verde?</a></li>
|
||||
<li><a href="#qué-significan-las-marcas-que-se-muestran-junto-a-los-mensajes-salientes">¿Qué significan las marcas que se muestran junto a los mensajes salientes?</a></li>
|
||||
<li><a href="#edit">Corregir errores y borrar mensajes después de enviar</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">¿Cómo funciona la desaparición de mensajes?</a></li>
|
||||
<li><a href="#delold">¿Qué pasa si activo “Borrar mensajes del dispositivo”?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
@@ -105,8 +105,7 @@
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat is a reliable, decentralized and secure instant messaging app,
|
||||
available for mobile and desktop platforms.</p>
|
||||
<p>Delta Chat es una aplicación de mensajería instantánea confiable, descentralizada y segura, disponible para dispositivos móviles y computadoras de escritorio.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
@@ -216,19 +215,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="delta-chat-soporta-envío-de-imágenes-videos-documentos-y-otros-archivos">
|
||||
|
||||
|
||||
¿Delta Chat soporta envío de imágenes, videos, documentos y otros archivos? <a href="#delta-chat-soporta-envío-de-imágenes-videos-documentos-y-otros-archivos" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes. 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>Para mejorar el rendimiento, las imágenes se optimizan y se envían en un tamaño más pequeño de forma predeterminada, pero puedes enviarla como un “archivo” para conservar la original.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -407,6 +393,32 @@ No se envían notificaciones y no hay límite de tiempo.</p>
|
||||
<p>Note, that the original message may still be received by chat members
|
||||
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1026,22 +1038,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1550,7 +1566,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/es/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+116
-171
@@ -2,27 +2,27 @@
|
||||
<html lang="fr"><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="#quest-ce-que-delta-chat-">Qu’est-ce que 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="#delta-chat-prend-il-en-charge-les-images-vidéos-et-autres-pièces-jointes-">Delta Chat prend-il en charge les images, vidéos et autres pièces jointes ?</a></li>
|
||||
<li><a href="#multiple-accounts">What are profiles? How can I switch between them?</a></li>
|
||||
<li><a href="#howtoe2ee">Comment trouver des personnes avec qui discuter ?</a></li>
|
||||
<li><a href="#pourquoi-une-discussion-est-marquée-comme-invitation-">Pourquoi une discussion est marquée comme “invitation” ?</a></li>
|
||||
<li><a href="#comment-mettre-deux-personnes-en-relation-entre-elles-">Comment mettre deux personnes en relation entre elles ?</a></li>
|
||||
<li><a href="#multiple-accounts">À quoi correspondent les profils ? Comment est-ce que je peux passer de l’un à l’autre ?</a></li>
|
||||
<li><a href="#qui-peut-voir-ma-photo-de-profil-">Qui peut voir ma photo de profil ?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
<li><a href="#signature">Est-il possible de définir une Bio/un Statut avec Delta Chat ?</a></li>
|
||||
<li><a href="#que-signifient-épingler-sourdine-et-archiver-">Que signifient “épingler”, “sourdine” et “archiver” ?</a></li>
|
||||
<li><a href="#save">How do “Saved Messages” work?</a></li>
|
||||
<li><a href="#save">Comment fonctionnent les “Messages enregistrés” ?</a></li>
|
||||
<li><a href="#que-signifie-le-point-vert-">Que signifie le point vert ?</a></li>
|
||||
<li><a href="#que-signifient-les-coches-affichées-à-côté-des-messages-sortants-">Que signifient les coches affichées à côté des messages sortants ?</a></li>
|
||||
<li><a href="#edit">Correct typos and delete messages after sending</a></li>
|
||||
<li><a href="#ephemeralmsgs">How do disappearing messages work?</a></li>
|
||||
<li><a href="#edit">Corriger des fautes et supprimer des messages après les avoir envoyés</a></li>
|
||||
<li><a href="#mediaquality">Comment est gérée la qualité des médias envoyés ?</a></li>
|
||||
<li><a href="#ephemeralmsgs">Comment fonctionnent les messages éphémères ?</a></li>
|
||||
<li><a href="#delold">Que se passe-t-il si j’active l’option “Supprimer les anciens messages de l’appareil” ?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
<li><a href="#remove-account">Comment puis-je supprimer un profil de discussion ?</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#groups">Groups</a>
|
||||
<li><a href="#groups">Groupes</a>
|
||||
<ul>
|
||||
<li><a href="#création-dun-groupe">Création d’un groupe</a></li>
|
||||
<li><a href="#addmembers">Add and remove members</a></li>
|
||||
<li><a href="#addmembers">Ajouter et supprimer des membres</a></li>
|
||||
<li><a href="#jai-quitté-un-groupe-par-accident">J’ai quitté un groupe par accident.</a></li>
|
||||
<li><a href="#je-ne-souhaite-plus-recevoir-les-messages-dun-groupe">Je ne souhaite plus recevoir les messages d’un groupe.</a></li>
|
||||
<li><a href="#cloning-a-group">Cloning a group</a></li>
|
||||
@@ -105,90 +105,71 @@
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Delta Chat is a reliable, decentralized and secure instant messaging app,
|
||||
available for mobile and desktop platforms.</p>
|
||||
<p>Delta Chat est une application de messagerie fiable, décentralisée et sécurisée, disponible pour smartphones et ordinateurs de bureau.</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>Création à la volée de <strong>profils de discussion privés</strong> grâce à des <a href="https://chatmail.at/relays">relais chatmail</a> sécurisés et interopérables qui permettent des échanges instantanés et des notifications Push pour les appareils iOS et Android.</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>Fonctionne en <a href="#multiple-accounts">multi-profils</a> et <a href="#multiclient">multi-appareils</a> sur toutes les plateformes et quelque soit <a href="https://chatmail.at/clients">l’application chatmail</a> utilisée.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Interactive <a href="#webxdc">in-chat apps</a> for gaming and collaboration</p>
|
||||
<p><a href="#webxdc">Applications</a> interactives permettant de jouer et collaborer au sein des discussions.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><a href="#security-audits">Audited end-to-end encryption</a>
|
||||
safe against network and server attacks.</p>
|
||||
<p><a href="#security-audits">Chiffrement de bout-en-bout audité</a> protégé contre les attaques réseaux et de serveurs.</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>Logiciel libre et gratuit, autant côté serveur que côté application, développé autour des <a href="https://github.com/chatmail/core/blob/main/standards.md#standards-used-in-delta-chat">standards d’internet</a>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="howtoe2ee">
|
||||
|
||||
|
||||
How can I find people to chat with? <a href="#howtoe2ee" class="anchor"></a>
|
||||
Comment trouver des personnes avec qui discuter ? <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>Delta Chat est une application de messagerie privée. Il n’y a pas de mécanisme de découverte publique des utilisateurs et utilisatrices. <em>Vous</em> décidez avec qui vous échangez dans Delta Chat.</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>En <strong>présence physique</strong> de vos amis ou de membres de votre famille, appuyez sur l’icône <strong>QR code</strong> <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> sur l’écran principal. Demandez à votre contact de <strong>scanner</strong> le QR code avec leur application 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>Pour la mise en place d’un échange <strong>à distance</strong>, cliquez sur l’icône <strong>QR code</strong> sur l’écran principal, puis sur le bouton “Lien”, puis sur “Copier” ou “Partager” et envoyez le lien d’invitation via une autre messagerie privée.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Now wait while connection gets established.</p>
|
||||
<p>Il suffit ensuite d’attendre que la connexion soit établie.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>If both sides are online, they will soon see a chat
|
||||
and can start messaging securely.</p>
|
||||
<p>Si les deux contacts sont en ligne, ils verront rapidement apparaître une nouvelle discussion et pourront tout de suite commencer une discussion sécurisée.</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>Si l’un des contacts n’est pas en ligne ou a des problèmes de réseau, la possibilité de démarrer la discussion aura lieu dès que la connectivité sera rétablie.</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>Félicitations !
|
||||
Vous utiliserez désormais le <a href="#e2ee">chiffrement de bout-en-bout</a> avec ce contact.
|
||||
Si vous vous ajoutez à des discussions de <a href="#groups">groupe</a> le chiffrement de bout-en-bout sera établi entre tous les membres de la discussion.</p>
|
||||
|
||||
<h3 id="why-is-a-chat-marked-as-request">
|
||||
<h3 id="pourquoi-une-discussion-est-marquée-comme-invitation-">
|
||||
|
||||
|
||||
Why is a chat marked as “Request”? <a href="#why-is-a-chat-marked-as-request" class="anchor"></a>
|
||||
Pourquoi une discussion est marquée comme “invitation” ? <a href="#pourquoi-une-discussion-est-marquée-comme-invitation-" 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>Delta Chat étant une messagerie privée, seules les personnes avec qui vous <a href="#howtoe2ee">partagez votre QR code ou lien d’invitation</a> peuvent vous écrire.</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>Vos contacts peuvent cependant partager votre profil avec d’autres personnes, ce qui apparaît alors comme une <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">invitation</b>.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
@@ -204,53 +185,37 @@ recevoir de messages de cette personne, nous vous conseillons de la <strong>bloq
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="how-can-i-put-two-of-my-friends-in-contact-with-each-other">
|
||||
<h3 id="comment-mettre-deux-personnes-en-relation-entre-elles-">
|
||||
|
||||
|
||||
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>
|
||||
Comment mettre deux personnes en relation entre elles ? <a href="#comment-mettre-deux-personnes-en-relation-entre-elles-" 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>Joignez le premier contact à votre discussion avec le second en utilisant <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Bouton pièce jointe → Contact</strong>.
|
||||
Profitez-en pour ajouter un message de présentation.</p>
|
||||
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="delta-chat-prend-il-en-charge-les-images-vidéos-et-autres-pièces-jointes-">
|
||||
|
||||
|
||||
Delta Chat prend-il en charge les images, vidéos et autres pièces jointes ? <a href="#delta-chat-prend-il-en-charge-les-images-vidéos-et-autres-pièces-jointes-" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Oui. 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>Pour améliorer les performances, les images sont redimensionnées et envoyées en taille réduite par défaut ; mais vous pouvez les envoyer en tant que “fichier” pour en conserver la taille originale.</p>
|
||||
<p>Le deuxième contact recevra une <strong>carte</strong> qu’il pourra cliquer pour commencer une discussion avec le premier.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
What are profiles? How can I switch between them? <a href="#multiple-accounts" class="anchor"></a>
|
||||
À quoi correspondent les profils ? Comment est-ce que je peux passer de l’un à l’autre ? <a href="#multiple-accounts" class="anchor"></a>
|
||||
|
||||
|
||||
</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>Un profil est <strong>un nom, une photo</strong> et quelques informations supplémentaires pour chiffrer des messages.
|
||||
Un profil n’est présent que sur votre appareil/vos appareils et utilise le serveur seulement pour relayer les messages vers vos contacts.</p>
|
||||
|
||||
<p>On first installation of Delta Chat a first profile is created.</p>
|
||||
<p>Lors de la première installation de Delta Chat un premier profil est créé.</p>
|
||||
|
||||
<p>Later, you can tap your profile image in the upper left corner to <strong>Add Profiles</strong>
|
||||
or to <strong>Switch Profiles</strong>.</p>
|
||||
<p>Par la suite vous pourrez cliquer sur votre image de profil en haut à gauche de l’écran principal pour <strong>Ajouter un profil</strong> ou <strong>Changer de profil</strong>.</p>
|
||||
|
||||
<p>You may want to use separate profiles for political, family or work related activities.</p>
|
||||
<p>Vous souhaiterez peut être utiliser des profils différents pour vos activités familiales, professionnelles ou politiques.</p>
|
||||
|
||||
<p>You may also wish to learn <a href="#multiclient">how to use the same profile on multiple devices</a>.</p>
|
||||
<p>Vous souhaiterez peut être aussi apprendre <a href="#multiclient">comment utiliser le même profil sur plusieurs appareils</a>.</p>
|
||||
|
||||
<h3 id="qui-peut-voir-ma-photo-de-profil-">
|
||||
|
||||
@@ -267,15 +232,13 @@ or to <strong>Switch Profiles</strong>.</p>
|
||||
<h3 id="signature">
|
||||
|
||||
|
||||
Can I set a Bio/Status with Delta Chat? <a href="#signature" class="anchor"></a>
|
||||
Est-il possible de définir une Bio/un Statut avec Delta Chat ? <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>Oui, vous pouvez le faire dans <strong>Paramètres → Profil → Signature</strong>.
|
||||
À partir du moment ou vous échangez avec quelqu’un, cette personne pourra voir votre Signature en regardant les détails de votre profil.</p>
|
||||
|
||||
<h3 id="que-signifient-épingler-sourdine-et-archiver-">
|
||||
|
||||
@@ -309,37 +272,33 @@ pour mettre une discussion en sourdine, ouvrez le menu de la conversation (Andro
|
||||
<h3 id="save">
|
||||
|
||||
|
||||
How do “Saved Messages” work? <a href="#save" class="anchor"></a>
|
||||
Comment fonctionnent les “Messages enregistrés” ? <a href="#save" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p><strong>Saved Messages</strong> is a chat that you can use to easily remember and find messages.</p>
|
||||
<p><strong>Messages enregistrés</strong> est une discussion que vous pouvez utiliser pour sauvegarder et retrouver facilement des messages.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>In any chat, long tap or right click a message and select <strong>Save</strong></p>
|
||||
<p>Dans n’importe quelle discussion, touchez <img style="vertical-align:middle; width:1.2em; margin:1px;" src="../saved-icon.png" alt="Saved icon" /> ou faites un clic droit sur un message et sélectionnez <strong>Enregistrer</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Saved messages are marked by the symbol
|
||||
<img style="vertical-align:middle; width:1.2em; margin:1px" src="../saved-icon.png" alt="Saved icon" />
|
||||
next to the timestamp</p>
|
||||
<p>Les messages enregistrés sont identifiés par le symbole <img style="vertical-align:middle; width:1.2em; margin:1px" src="../saved-icon.png" alt="Saved icon" /> à côté de l’horodatage du message.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Later, open the “Saved Messages” chat - and you will see the saved messages there.
|
||||
By tapping <img style="vertical-align:middle; width:1.2em; margin:1px" src="../go-to-original.png" alt="Arrow-right icon" />,
|
||||
you can go back to the original message in the original chat</p>
|
||||
<p>Par la suite vous pourrez retrouver tous ces messages dans la discussion “Messages enregistrés”.
|
||||
En touchant <img style="vertical-align:middle; width:1.2em; margin:1px" src="../go-to-original.png" alt="Arrow-right icon" /> vous retrouverez le message dans sa discussion d’origine.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Finally, you can also use “Saved Messages” to take <strong>personal notes</strong> - open the chat, type something, add a photo or a voice message etc.</p>
|
||||
<p>Enfin vous pouvez aussi utiliser la discussion “Messages enregistrés” pour prendre des <strong>notes privées</strong> - ouvrez la discussion, écrivez votre message, ajoutez une photo, un message vocal, etc.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>As “Saved Message” are synced, they can become very handy for transferring data between devices</p>
|
||||
<p>La discussion “Messages enregistrés” étant synchronisée elle peut être très pratique pour transférer des informations d’un appareil à un autre.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Messages stay saved even if they are edited or deleted -
|
||||
may it be by <a href="#edit">sender</a>, by <a href="#delold">device cleanup</a> or by <a href="#ephemeralmsgs">disappearing messages of other chats</a>.</p>
|
||||
<p>Les messages restent enregistrés même lorsqu’ils sont édités ou supprimés, que ça soit par <a href="#edit">l’expéditeur⋅rice</a>, par <a href="#delold">suppression des anciens messages de l’appareil</a> ou par <a href="#ephemeralmsgs">disparition des messages éphémères au sein d’une discussion</a>.</p>
|
||||
|
||||
<h3 id="que-signifie-le-point-vert-">
|
||||
|
||||
@@ -349,13 +308,10 @@ may it be by <a href="#edit">sender</a>, by <a href="#delold">device cleanup</a>
|
||||
|
||||
</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>Vous pourrez parfois voir un <strong>point vert</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" /> sur le profil d’un contact.
|
||||
Cela veut dire que vous avez <strong>récemment vu</strong> le contact dans les 10 dernières minutes, par exemple parce que vous avez échangé avec ce contact, ou que vous avez reçu un accusé de lecture.</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>Ceci n’est donc pas un indicateur temps réel de présence en ligne et vos contacts ne pourront donc pas toujours voir si vous êtes en ligne ou non.</p>
|
||||
|
||||
<h3 id="que-signifient-les-coches-affichées-à-côté-des-messages-sortants-">
|
||||
|
||||
@@ -367,79 +323,75 @@ 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>Une marque</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick1.png" alt="" /> veut dire que le message a été envoyé correctement au <a href="#relays">relai</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>Deux marques</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick2.png" alt="" /> indiquent que votre contact a lu le message.</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>Au sein des <a href="#groups">groupes</a> la seconde marque veut dire qu’au moins un membre du groupe a pu lire le message.</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>La seconde marque ne peut s’afficher que si vous et l’un de vos destinataires du message avez activé <strong>Paramètres → Discussions → Accusés de lecture</strong>.</p>
|
||||
|
||||
<h3 id="edit">
|
||||
|
||||
|
||||
Correct typos and delete messages after sending <a href="#edit" class="anchor"></a>
|
||||
Corriger des fautes et supprimer des messages après les avoir envoyés <a href="#edit" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can edit the text of your messages after sending.
|
||||
For that, long tap or right click the message and select <strong>Edit</strong>
|
||||
or <img style="vertical-align:middle; width:1.2em; margin:1px" src="../edit-icon.png" alt="Edit icon" />.</p>
|
||||
<p>Vous pouvez éditer le texte de votre message après l’avoir envoyé. Pour cela, appuyer longtemps ou faire clic droit sur le message et choisir <strong>Modifier</strong> ou <img style="vertical-align:middle; width:1.2em; margin:1px" src="../edit-icon.png" alt="Edit icon" />.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you have sent a message accidentally,
|
||||
from the same menu, select <strong>Delete</strong> and then <strong>Delete for Everyone</strong>.</p>
|
||||
<p>Si vous avez envoyé un message par erreur vous pouvez le supprimer depuis le même menu en choisissant <strong>Supprimer le message</strong> puis <strong>Supprimer pour moi</strong> ou <strong>Supprimer pour tout le monde</strong>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>While edited messages will have the word “Edited” next to the timestamp,
|
||||
deleted messages will be removed without a marker in the chat.
|
||||
Notifications are not sent and there is no time limit.</p>
|
||||
<p>Les messages modifiés seront indiqués par le mot “Modifié” à côté de l’horodatage du message, tandis que les messages supprimés le sont sans notification, ni dans la discussion, ni sur l’appareil, et peuvent l’être sans limite dans le temps.</p>
|
||||
|
||||
<p>Note, that the original message may still be received by chat members
|
||||
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
|
||||
<p>Remarque : le message original peut toutefois encore exister s’il a déjà été transféré, enregistré, ou si l’un des membres de la discussion en a fait une copie ou une capture d’écran.</p>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How do disappearing messages work? <a href="#ephemeralmsgs" class="anchor"></a>
|
||||
Comment est gérée la qualité des médias envoyés ? <a href="#mediaquality" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>You can turn on “disappearing messages”
|
||||
in the settings of a chat,
|
||||
at the top right of the chat window,
|
||||
by selecting a time span
|
||||
between 5 minutes and 1 year.</p>
|
||||
<p>Images, vidéos, fichiers, messages vocaux etc. peuvent être envoyé avec les boutons <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Pièce jointe</strong> ou <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Message vocal</strong>.</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>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Par défaut le niveau de compression choisi permet un <strong>envoi rapide et efficace</strong> des messages en respectant les limites de données et de stockage de tout le monde. C’est un réglage idéal pour des échanges standards.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Dans les régions avec une connectivité limitée il est possible de choisir une compression plus forte dans <strong>Paramètres → Discussions → Qualité du média en sortie</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Si vous souhaitez absolument transmettre un fichier en <strong>qualité originale</strong>, utilisez <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Pièce jointe → Fichier</strong> dans la discussion. Pensez à utiliser cette fonctionnalité avec parcimonie pour limiter l’utilisation des données par toutes les personnes dans la discussion.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
Comment fonctionnent les messages éphémères ? <a href="#ephemeralmsgs" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Note that you can rely on disappearing messages
|
||||
only as long as you trust your chat partners;
|
||||
malicious chat partners can take photos,
|
||||
or otherwise save, copy or forward messages before deletion.</p>
|
||||
<p>Vous pouvez activer les “Messages éphémères” dans les paramètres d’une discussion, dans le menu en haut à droite, en choisissant une durée prédéfinie entre 5 min et 1 an.</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>Tant que le paramètre reste activé, les applications de chacun des membres se chargent de supprimer les messages qui dépassent la durée définie. La durée est comptabilisée à partir du moment où le message est vu pour la première fois, et le message est supprimé au sein de l’application et sur le serveur.</p>
|
||||
|
||||
<p>Remarque : ne faites confiance au réglage “Messages éphémères” que si vous faites confiance aux membres de la discussion. Des membres mal intentionné⋅es peuvent très bien prendre des photos, copier, sauvegarder, transférer un message avant qu’il ne soit supprimé.</p>
|
||||
|
||||
<p>Enfin, si l’un⋅e des membres d’une discussion désinstalle l’application Delta Chat, les messages (chiffrés, pour rappel) peuvent rester plus longtemps sur le serveur avant d’être supprimés.</p>
|
||||
|
||||
<h3 id="delold">
|
||||
|
||||
@@ -456,39 +408,28 @@ the (anyway encrypted) messages may take longer to get deleted from their server
|
||||
<h3 id="remove-account">
|
||||
|
||||
|
||||
How can I delete my chat profile? <a href="#remove-account" class="anchor"></a>
|
||||
Comment puis-je supprimer un profil de discussion ? <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>Si vous utilisez plus d’un profil dans l’application, vous pouvez supprimer un profil spécifique via le menu (en haut sur Android et iOS), ou dans la barre latérale (sur l’application de bureau) permettant de passer d’un profil à un autre. Les profils ne sont toujours supprimés que sur l’appareil en question. Les profils continueront donc d’exister sur les autres appareils le cas échéant.</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>Si vous n’utilisez qu’un seul profil vous pouvez tout simplement désinstaller l’application. Cela déclenchera la suppression des données liées au profil en question sur le serveur chatmail. Pour plus d’informations voir <a href="https://nine.testrun.org/info.html#account-deletion">Account deletion sur nine.testrun.org</a> ou la page respective du <a href="https://chatmail.at/relays">serveur chatmail</a> que vous utilisez.</p>
|
||||
|
||||
<h2 id="groups">
|
||||
|
||||
|
||||
Groups <a href="#groups" class="anchor"></a>
|
||||
Groupes <a href="#groups" class="anchor"></a>
|
||||
|
||||
|
||||
</h2>
|
||||
|
||||
<p>Groups let several people chat together privately with <strong>equal rights</strong>.</p>
|
||||
<p>Les groupes permettent à plusieurs personnes d’avoir une discussion. Chaque membre du groupe à les <strong>même droits</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>Chaque membre du groupe peut changer le nom du groupe ou l’avatar, <a href="#addmembers">ajouter ou supprimer des membres</a>, activer/désactiver <a href="#ephemeralmsgs">les messages éphémères</a>, et <a href="#edit">supprimer leurs propres messages</a> sur tous les appareils des membres de la discussion.</p>
|
||||
|
||||
<p>Because all members have the same rights, groups work best among <strong>trusted friends and family</strong>.</p>
|
||||
<p>Puisque tous les membres d’une discussion ont les mêmes droits, les groupes fonctionnent mieux avec des <strong>personnes de confiance et la famille</strong>.</p>
|
||||
|
||||
<h3 id="création-dun-groupe">
|
||||
|
||||
@@ -513,7 +454,7 @@ and <a href="#edit">delete their own messages</a> from all member’s devices.</
|
||||
<h3 id="addmembers">
|
||||
|
||||
|
||||
Add and remove members <a href="#addmembers" class="anchor"></a>
|
||||
Ajouter et supprimer des membres <a href="#addmembers" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
@@ -1016,22 +957,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1538,7 +1483,7 @@ research paper published afterwards.</p>
|
||||
Vous trouverez <a href="https://delta.chat/en/2023-05-22-webxdc-security">ici un article de fond complet à propos de la sécurité du chiffrement de bout-en-bout sur internet</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Début 2023, <a href="https://cure53.de">Cure53</a> a analysé le chiffrement d’acheminement des connexions réseau de Delta Chat et testé une configuration de serveur de courriel reproductible, telle que <a href="https://delta.chat/fr/serverguide">recommandée sur ce site</a>.
|
||||
<p>Début 2023, <a href="https://cure53.de">Cure53</a> a analysé le chiffrement d’acheminement des connexions réseau de Delta Chat et testé une configuration de serveur de courriel reproductible, telle que <a href="https://delta.chat/serverguide">recommandée sur ce site</a>.
|
||||
Vous trouverez plus d’informations sur cet audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">sur notre blog</a> ou dans <a href="https://delta.chat/assets/blog/MER-01-report.pdf">le rapport complet ici</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="#apakah-delta-chat-mendukung-gambar-vidio-dan-lampiran-lainnya">Apakah Delta Chat mendukung gambar, vidio dan lampiran lainnya?</a></li>
|
||||
<li><a href="#multiple-accounts">What are profiles? How can I switch between them?</a></li>
|
||||
<li><a href="#siapa-yang-dapat-melihat-foto-profil-saya">Siapa yang dapat melihat Foto Profil saya?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#what-does-the-green-dot-mean">What does the green dot mean?</a></li>
|
||||
<li><a href="#what-do-the-ticks-shown-beside-outgoing-messages-mean">What do the ticks shown beside outgoing messages mean?</a></li>
|
||||
<li><a href="#edit">Correct typos and delete messages after sending</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">How do disappearing messages work?</a></li>
|
||||
<li><a href="#delold">What happens if I turn on “Delete Messages from Device”?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
@@ -218,19 +218,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="apakah-delta-chat-mendukung-gambar-vidio-dan-lampiran-lainnya">
|
||||
|
||||
|
||||
Apakah Delta Chat mendukung gambar, vidio dan lampiran lainnya? <a href="#apakah-delta-chat-mendukung-gambar-vidio-dan-lampiran-lainnya" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes. 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>For performance, images are optimized and sent at a smaller size by default, but you can send it as a “file” to preserve the original.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -411,6 +398,32 @@ Notifications are not sent and there is no time limit.</p>
|
||||
<p>Note, that the original message may still be received by chat members
|
||||
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1030,22 +1043,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1561,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/id/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<li><a href="#howtoe2ee">Come posso trovare persone con cui chattare?</a></li>
|
||||
<li><a href="#perché-una-chat-è-contrassegnata-come-richiesta">Perché una chat è contrassegnata come “Richiesta”?</a></li>
|
||||
<li><a href="#come-posso-mettere-in-contatto-due-miei-amici">Come posso mettere in contatto due miei amici?</a></li>
|
||||
<li><a href="#delta-chat-supporta-immagini-video-e-altri-allegati">Delta Chat supporta immagini, video e altri allegati?</a></li>
|
||||
<li><a href="#multiple-accounts">Cosa sono i profili? Come posso passare dall’uno all’altro?</a></li>
|
||||
<li><a href="#chi-vede-la-mia-immagine-del-profilo">Chi vede la mia immagine del profilo?</a></li>
|
||||
<li><a href="#signature">Posso impostare una Biografia/Stato con Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#cosa-significa-il-punto-verde">Cosa significa il punto verde?</a></li>
|
||||
<li><a href="#cosa-significano-i-segni-di-spunta-visualizzati-accanto-ai-messaggi-in-uscita">Cosa significano i segni di spunta visualizzati accanto ai messaggi in uscita?</a></li>
|
||||
<li><a href="#edit">Correggi gli errori e cancella i messaggi dopo averli inviati</a></li>
|
||||
<li><a href="#mediaquality">Come viene gestita la qualità dei media?</a></li>
|
||||
<li><a href="#ephemeralmsgs">Come funzionano i messaggi a scomparsa?</a></li>
|
||||
<li><a href="#delold">Cosa succede se attivo “Elimina Messaggi dal Dispositivo”?</a></li>
|
||||
<li><a href="#remove-account">Come posso eliminare il mio profilo chat?</a></li>
|
||||
@@ -217,19 +217,6 @@ Puoi anche aggiungere un breve messaggio di presentazione.</p>
|
||||
<p>Il secondo contatto riceverà una <strong>scheda</strong>
|
||||
e potrà toccarla per iniziare a chattare con il primo contatto.</p>
|
||||
|
||||
<h3 id="delta-chat-supporta-immagini-video-e-altri-allegati">
|
||||
|
||||
|
||||
Delta Chat supporta immagini, video e altri allegati? <a href="#delta-chat-supporta-immagini-video-e-altri-allegati" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Sì. Immagini, video, files, messaggi vocali ecc. possono essere inviati utilizzando <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Allegato-</strong>
|
||||
o <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> pulsanti <strong>Messaggio Vocale</strong></p>
|
||||
|
||||
<p>Per le prestazioni, le immagini sono ottimizzate e inviate in dimensioni inferiori per impostazione predefinita, ma è possibile inviarle come “file” per preservare l’originale.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -409,6 +396,32 @@ Non vengono inviate notifiche e non c’è limite di tempo.</p>
|
||||
<p>Nota che il messaggio originale potrebbe essere ancora sui dispositivi dei membri della chat
|
||||
che avrebbero già potuto rispondere, inoltrare, salvare, scattare una schermata o copiare il messaggio in altri modi.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
Come viene gestita la qualità dei media? <a href="#mediaquality" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Immagini, video, files, messaggi vocali ecc. possono essere inviati utilizzando <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Allegato-</strong>
|
||||
o <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> pulsanti <strong>Messaggio Vocale</strong></p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Per impostazione predefinita, la compressione garantisce una <strong>consegna rapida ed efficiente</strong> che rispetta i limiti di dati e di spazio di archiviazione di tutti.
|
||||
Questa soluzione è ideale per la comunicazione quotidiana.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Nelle regioni con connettività peggiore,
|
||||
è possibile scegliere una compressione più elevata in <strong>Impostazioni → Chat → Qualità Media in Uscita</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Se hai bisogno di inviare i file multimediali nella loro <strong>qualità originale</strong>, usa <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Allega → File</strong> nella chat.
|
||||
Si prega di utilizzare questo metodo con parsimonia, poiché l’invio di file originali aumenterà significativamente il consumo di dati per te e per tutti i destinatari della chat.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1021,16 +1034,22 @@ Tuttavia, se lo si desidera,
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Puoi <strong>aggiungere</strong> un ripetitore scansionando il suo codice QR;<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> ne mostra alcuni noti.
|
||||
Se hai più ripetitori, riceverai messaggi su tutti.</p>
|
||||
<p>Puoi <strong>aggiungere</strong> un ripetitore scansionando il suo codice QR;
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> mostra alcuni ripetitori noti.
|
||||
Se hai più ripetitori, riceverai i messaggi su tutti.
|
||||
I contatti vengono a conoscenza automaticamente dei tuoi ripetitori attuali quando invii loro un messaggio.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>L’opzione <strong>predefinita</strong> definisce quella a cui i tuoi partner di chat invieranno messaggi futuri.</p>
|
||||
<p>Tocca un ripetitore per impostarlo come <strong>utilizzato per l’invio</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Se <strong>rimuovi</strong> un ripetitore,
|
||||
assicurati che un altro ripetitore predefinito sia stato utilizzato per un periodo di tempo sufficiente.
|
||||
In caso contrario, i messaggi dei tuoi interlocutori di chat non ti arriveranno.In caso di dubbio, rimuovilo in seguito.</p>
|
||||
i contatti che conoscono solo quel ripetitore potrebbero non raggiungerti finché non invierai loro un nuovo messaggio.
|
||||
Per rimanere raggiungibile nel frattempo, seleziona <strong>Nascondi dai Contatti</strong> nella finestra di dialogo di conferma
|
||||
invece di rimuoverlo immediatamente.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Per <strong>visualizzare</strong> nuovamente un ripetitore nascosto, toccalo.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1547,7 +1566,7 @@ Vedi <a href="https://delta.chat/en/2023-05-22-webxdc-security">qui per la stori
|
||||
<li>
|
||||
<p>A partire dal 2023, <a href="https://cure53.de">Cure53</a> ha analizzato sia la crittografia del trasporto delle
|
||||
Connessioni di rete di Delta Chat e una configurazione del server di posta riproducibile come
|
||||
<a href="https://delta.chat/it/serverguide">consigliato su questo sito</a>.
|
||||
<a href="https://delta.chat/serverguide">consigliato su questo sito</a>.
|
||||
Puoi leggere ulteriori informazioni sull’audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">sul nostro blog</a>
|
||||
o leggere il <a href="https://delta.chat/assets/blog/MER-01-report.pdf">rapporto completo qui</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="#ondersteunt-delta-chat-afbeeldingen-videos-en-ander-soort-bijlagen">Ondersteunt Delta Chat afbeeldingen, video’s en ander soort bijlagen?</a></li>
|
||||
<li><a href="#multiple-accounts">What are profiles? How can I switch between them?</a></li>
|
||||
<li><a href="#wie-kan-mijn-profielfoto-zien">Wie kan mijn profielfoto zien?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#wat-betekent-die-groene-stip">Wat betekent die groene stip?</a></li>
|
||||
<li><a href="#wat-betekenen-de-vinkjes-naast-verzonden-berichten">Wat betekenen de vinkjes naast verzonden berichten?</a></li>
|
||||
<li><a href="#edit">Correct typos and delete messages after sending</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">How do disappearing messages work?</a></li>
|
||||
<li><a href="#delold">Wat gebeurt er als ik ‘Oude berichten van server verwijderen’ inschakel?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
@@ -218,19 +218,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="ondersteunt-delta-chat-afbeeldingen-videos-en-ander-soort-bijlagen">
|
||||
|
||||
|
||||
Ondersteunt Delta Chat afbeeldingen, video’s en ander soort bijlagen? <a href="#ondersteunt-delta-chat-afbeeldingen-videos-en-ander-soort-bijlagen" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes. 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>Om de prestaties te verhogen, worden afbeeldingen standaard geoptimaliseerd en verkleind verstuurd, maar je kunt ze als een bestand verzenden om het origineel te sturen.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -410,6 +397,32 @@ Notifications are not sent and there is no time limit.</p>
|
||||
<p>Note, that the original message may still be received by chat members
|
||||
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1024,22 +1037,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1555,7 +1572,7 @@ Lees <a href="https://delta.chat/en/2023-05-22-webxdc-security">hier het volledi
|
||||
<li>
|
||||
<p>Aan het begin van 2023 heeft <a href="https://cure53.de">Cure53</a> de transportversleuteling van
|
||||
Delta Chats netwerkverbindingen getest, evenals de e-mailserveropzet zoals
|
||||
<a href="https://delta.chat/nl/serverguide">beschreven op onze site</a>.
|
||||
<a href="https://delta.chat/serverguide">beschreven op onze site</a>.
|
||||
Meer informatie over deze test is te lezen <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">op ons blog</a>
|
||||
of in het <a href="https://delta.chat/assets/blog/MER-01-report.pdf">volledige verslag</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="#czy-delta-chat-obsługuje-obrazy-filmy-i-inne-załączniki">Czy Delta Chat obsługuje obrazy, filmy i inne załączniki?</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>
|
||||
@@ -14,6 +13,7 @@
|
||||
<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="#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>
|
||||
@@ -215,19 +215,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="czy-delta-chat-obsługuje-obrazy-filmy-i-inne-załączniki">
|
||||
|
||||
|
||||
Czy Delta Chat obsługuje obrazy, filmy i inne załączniki? <a href="#czy-delta-chat-obsługuje-obrazy-filmy-i-inne-załączniki" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Tak. 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>Ze względu na wydajność obrazy są domyślnie optymalizowane i wysyłane w mniejszym rozmiarze, ale można je wysłać jako „plik”, aby zachować oryginał.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -392,6 +379,32 @@ has <strong>Settings → Chats → Read Receipts</strong> enabled.</p>
|
||||
|
||||
<p>Pamiętaj, że oryginalną wiadomość nadal mogą otrzymać członkowie czatu, którzy mogli już odpowiedzieć, przesłać dalej, zapisać, wykonać zrzut ekranu lub w inny sposób skopiować wiadomość.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -946,22 +959,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1396,7 +1413,7 @@ od najnowszych do najstarszych:</p>
|
||||
<p>W kwietniu 2023 r. naprawiliśmy problemy z bezpieczeństwem i prywatnością w funkcji „aplikacje internetowe udostępniane na czacie”, związane z awariami piaskownicy, szczególnie w przypadku Chromium. Następnie przeprowadziliśmy niezależny audyt bezpieczeństwa od Cure53 i wszystkie wykryte problemy zostały naprawione w aplikacji z serii 1.36 wydanej w kwietniu 2023 r. <a href="https://delta.chat/en/2023-05-22-webxdc-security">Pełną historię bezpieczeństwa end-to-end w sieci można znaleźć tutaj</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>W marcu 2023 r. firma <a href="https://cure53.de">Cure53</a> przeanalizowała zarówno szyfrowanie transportu połączeń sieciowych Delta Chat, jak i powtarzalną konfigurację serwera pocztowego zgodnie z <a href="https://delta.chat/pl/serverguide">zaleceniami na tej stronie</a>. Możesz przeczytać więcej o audycie <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">na naszym blogu</a> lub przeczytać pełny raport <a href="https://delta.chat/assets/blog/MER-01-report.pdf">tutaj</a>.</p>
|
||||
<p>W marcu 2023 r. firma <a href="https://cure53.de">Cure53</a> przeanalizowała zarówno szyfrowanie transportu połączeń sieciowych Delta Chat, jak i powtarzalną konfigurację serwera pocztowego zgodnie z <a href="https://delta.chat/serverguide">zaleceniami na tej stronie</a>. Możesz przeczytać więcej o audycie <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">na naszym blogu</a> lub przeczytać pełny raport <a href="https://delta.chat/assets/blog/MER-01-report.pdf">tutaj</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>W 2020 r. firma <a href="https://includesecurity.com">Include Security</a> przeanalizowała biblioteki Rust <a href="https://github.com/deltachat/deltachat-core-rust/">core</a>, <a href="https://github.com/async-email/async-imap">IMAP</a>, <a href="https://github.com/async-email/async-smtp">SMTP</a> i <a href="https://github.com/async-email/async-native-tls">TLS</a> Delta Chat. Nie znalazła żadnych problemów krytycznych ani poważnych. W raporcie zwrócono uwagę na kilka słabych punktów o średniej wadze – same w sobie nie stanowią zagrożenia dla użytkowników Delta Chat, ponieważ zależą od środowiska, w którym używany jest Delta Chat. Ze względu na użyteczność i kompatybilność nie możemy złagodzić wszystkich z nich i zdecydowaliśmy się przedstawić zalecenia dotyczące bezpieczeństwa zagrożonym użytkownikom. Pełny raport można przeczytać <a href="https://delta.chat/assets/blog/2020-second-security-review.pdf">tutaj</a>.</p>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<li><a href="#howtoe2ee">Como posso encontrar pessoas para conversar?</a></li>
|
||||
<li><a href="#por-que-um-chat-está-marcado-como-solicitação">Por que um chat está marcado como “Solicitação”?</a></li>
|
||||
<li><a href="#como-posso-colocar-dois-dos-meus-amigos-em-contato-um-com-o-outro">Como posso colocar dois dos meus amigos em contato um com o outro?</a></li>
|
||||
<li><a href="#dá-para-mandar-imagens-vídeos-e-outros-anexos-pelo-delta-chat">Dá para mandar imagens, vídeos e outros anexos pelo Delta Chat?</a></li>
|
||||
<li><a href="#multiple-accounts">O que são perfis? Como posso alternar entre eles?</a></li>
|
||||
<li><a href="#quem-consegue-ver-a-imagem-do-meu-perfil">Quem consegue ver a imagem do meu perfil?</a></li>
|
||||
<li><a href="#signature">Posso definir uma Bio/Assinatura com o Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#o-que-significa-o-ponto-verde">O que significa o ponto verde?</a></li>
|
||||
<li><a href="#o-que-significam-os-carrapatos-mostrados-ao-lado-das-mensagens-de-saída">O que significam os carrapatos mostrados ao lado das mensagens de saída?</a></li>
|
||||
<li><a href="#edit">Corrigir erros de digitação e excluir mensagens após o envio</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">Como funcionam as mensagens efêmeras?</a></li>
|
||||
<li><a href="#delold">O que acontece se eu ativar a opção “Apagar mensagens do dispositivo”?</a></li>
|
||||
<li><a href="#remove-account">Como posso excluir minha conta?</a></li>
|
||||
@@ -218,19 +218,6 @@ Você também pode adicionar uma pequena mensagem de apresentação.</p>
|
||||
<p>O segundo contato receberá um <strong>cartão</strong>
|
||||
e poderá tocar nele para começar a conversar com o primeiro contato.</p>
|
||||
|
||||
<h3 id="dá-para-mandar-imagens-vídeos-e-outros-anexos-pelo-delta-chat">
|
||||
|
||||
|
||||
Dá para mandar imagens, vídeos e outros anexos pelo Delta Chat? <a href="#dá-para-mandar-imagens-vídeos-e-outros-anexos-pelo-delta-chat" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Sim. Imagens, vídeos, arquivos, mensagens de voz etc. podem ser enviados usando os botões de<img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Anexo-</strong>
|
||||
ou <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Mensagem de Voz</strong></p>
|
||||
|
||||
<p>Para fins de desempenho, as imagens são otimizadas e enviadas em um tamanho menor por padrão, mas você pode enviá-las como um “arquivo” para preservar o original.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -409,6 +396,32 @@ Não são enviadas notificações e não há limite de tempo.</p>
|
||||
<p>Observe que a mensagem original ainda pode ser recebida pelos membros do conversa
|
||||
que podem ter respondido, encaminhado, salvo, capturado a tela ou copiado a mensagem.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <a href="#mediaquality" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Imagens, vídeos, arquivos, mensagens de voz etc. podem ser enviados usando os botões de<img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Anexo-</strong>
|
||||
ou <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Mensagem de Voz</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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1025,22 +1038,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1556,7 +1573,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/pt/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<li><a href="#howtoe2ee">Как найти людей для общения?</a></li>
|
||||
<li><a href="#почему-чат-помечен-как-запрос">Почему чат помечен как “Запрос”?</a></li>
|
||||
<li><a href="#как-я-могу-связать-двух-своих-друзей-друг-с-другом">Как я могу связать двух своих друзей друг с другом?</a></li>
|
||||
<li><a href="#поддерживает-ли-delta-chat-изображения-видео-и-другие-вложения">Поддерживает ли Delta Chat изображения, видео и другие вложения?</a></li>
|
||||
<li><a href="#multiple-accounts">Что такое профили? Как я могу переключатся между ними?</a></li>
|
||||
<li><a href="#кто-видит-изображение-моего-профиля">Кто видит изображение моего профиля?</a></li>
|
||||
<li><a href="#signature">Могу ли я установить статус/подпись в Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#что-означает-зеленая-точка">Что означает зеленая точка?</a></li>
|
||||
<li><a href="#что-означают-галочки-рядом-с-исходящими-сообщениями">Что означают галочки рядом с исходящими сообщениями?</a></li>
|
||||
<li><a href="#edit">Исправление опечаток и удаление сообщений после отправки</a></li>
|
||||
<li><a href="#mediaquality">Как обеспечивается качество мультимедиа?</a></li>
|
||||
<li><a href="#ephemeralmsgs">Как работают исчезающие сообщения?</a></li>
|
||||
<li><a href="#delold">Что произойдет, если я включу функцию “Удалять сообщения с устройства”?</a></li>
|
||||
<li><a href="#remove-account">Как удалить свой профиль в чате?</a></li>
|
||||
@@ -214,19 +214,6 @@
|
||||
<p>Второй контакт получит <strong>карточку</strong>
|
||||
на которую можно нажать, чтобы начать общение с первым контактом.</p>
|
||||
|
||||
<h3 id="поддерживает-ли-delta-chat-изображения-видео-и-другие-вложения">
|
||||
|
||||
|
||||
Поддерживает ли Delta Chat изображения, видео и другие вложения? <a href="#поддерживает-ли-delta-chat-изображения-видео-и-другие-вложения" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Да. Изображения, видео, файлы, голосовые сообщения и т.д. можно отправлять с помощью кнопок <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Вложение</strong>
|
||||
или <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Голосовое сообщение</strong>.</p>
|
||||
|
||||
<p>Для повышения производительности, изображения оптимизируются и отправляются по умолчанию в уменьшенном размере, но вы можете отправить их как “файл”, чтобы сохранить оригинал.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -406,6 +393,32 @@
|
||||
<p>Обратите внимание, что исходное сообщение все еще может быть получено участниками чата
|
||||
которые могли уже ответить, переслать, сохранить, сделать скриншот или иным образом скопировать сообщение.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
Как обеспечивается качество мультимедиа? <a href="#mediaquality" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Изображения, видео, файлы, голосовые сообщения и т.д. можно отправлять с помощью кнопок <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Вложение</strong>
|
||||
или <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Голосовое сообщение</strong>.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>По умолчанию сжатие обеспечивает <strong>быструю и эффективную доставку</strong> сообщений, учитывая ограничения по объему данных и хранилищу у всех пользователей.
|
||||
Это идеально подходит для повседневного общения.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В регионах с плохим качеством связи,
|
||||
вы можете выбрать более высокую степень сжатия в меню <strong>Настройки → Чаты → Качество отправляемых медиафайлов</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Если вам необходимо отправить мультимедийный файл в <strong>исходном качестве</strong>, используйте значок <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Прикрепить → Файл</strong> в чате.
|
||||
Используйте этот метод с осторожностью, так как отправка файлов в исходном качестве значительно увеличит объем трафика как для вас, так и для всех участников чата.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1024,22 +1037,26 @@ PIN-код разблокировки экрана, графический кл
|
||||
<p>По умолчанию, после установки, релей <strong>настраивается автоматически</strong>,
|
||||
поэтому вам не нужно об этом волноваться.
|
||||
Однако, если хотите,
|
||||
вы можете настроить релеи в <strong>Настройки → Дополнительно → Релеи</strong>:</p>
|
||||
вы можете настроить релеи в меню <strong>Настройки → Дополнительно → Релеи</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>Вы можете <strong>добавить</strong> релей, отсканировав его QR-код;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> содержит список известных релеев.
|
||||
Если у вас несколько релеев, вы будете получать сообщения на все из них.</p>
|
||||
на сайте <a href="https://chatmail.at/relays">chatmail.at/relays</a> представлен список известных.
|
||||
Если у вас настроено несколько релеев, сообщения будут доставляться через все из них.
|
||||
Ваши контакты автоматически узнают о текущих используемых вами релеях при обмене сообщениями.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>По умолчанию</strong> определяет релей, на который ваши собеседники будут отправлять будущие сообщения.</p>
|
||||
<p>Нажмите на релей, чтобы установить его как <strong>используемый для отправки</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Если вы <strong>удаляете</strong> релей,
|
||||
убедитесь, что другой релей по умолчанию использовался в течение достаточного количества времени.
|
||||
В противном случае сообщения от ваших собеседников не будут доходить до вас.
|
||||
Если сомневаетесь, удалите его позже.</p>
|
||||
<p>Если вы <strong>удалите</strong> релей,
|
||||
контакты, которые знают только этот релей, не смогут связаться с вами до тех пор, пока вы снова не напишите им.
|
||||
Чтобы оставаться на связи в это время, выберите опцию <strong>Скрыть из контактов</strong> во всплывающем окне
|
||||
вместо того, чтобы удалять его сразу.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Чтобы снова <strong>показать</strong> скрытый релей, нажмите на него.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1554,11 +1571,10 @@ Applied Cryptography в ETH Цюрихе и устранили все выявл
|
||||
См. здесь <a href="https://delta.chat/en/2023-05-22-webxdc-security">полную информацию о безопасности сквозного шифрования в Интернете</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>В Марте 2023 года, <a href="https://cure53.de">Cure53</a> проанализировал как протокол защиты транспортного уровня
|
||||
сетевого соединения Delta Chat, так и воспроизводимую установку почтового сервера,
|
||||
<a href="https://delta.chat/ru/serverguide">рекомендуемую на этом сайте</a>.
|
||||
Подробнее об аудите можно узнать <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">в нашем блоге</a>
|
||||
или прочитать <a href="https://delta.chat/assets/blog/MER-01-report.pdf">полный отчёт здесь</a>.</p>
|
||||
<p>В марте 2023 года компания <a href="https://cure53.de">Cure53</a> провела анализ шифрования транспортного уровня сетевых соединений Delta Chat, а также проверку воспроизводимой конфигурации почтового сервера,
|
||||
<a href="https://delta.chat/serverguide">рекомендованной на этом сайте</a>.
|
||||
Подробнее об аудите можно узнать <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">в нашем блоге</a>,
|
||||
а <a href="https://delta.chat/assets/blog/MER-01-report.pdf">полный отчет доступен по этой ссылке</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>2020 год, <a href="https://includesecurity.com">Include Security</a> проанализировала Delta
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="#podporuje-delta-chat-obrázky-videá-a-iné-prílohy">Podporuje Delta Chat obrázky, videá a iné prílohy?</a></li>
|
||||
<li><a href="#multiple-accounts">What are profiles? How can I switch between them?</a></li>
|
||||
<li><a href="#kto-vidí-moju-profilovú-fotku">Kto vidí moju profilovú fotku?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#what-does-the-green-dot-mean">What does the green dot mean?</a></li>
|
||||
<li><a href="#čo-znamenajú-zaškrtnutia-zobrazené-vedľa-odchádzajúcich-správ">Čo znamenajú zaškrtnutia zobrazené vedľa odchádzajúcich správ?</a></li>
|
||||
<li><a href="#edit">Correct typos and delete messages after sending</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">How do disappearing messages work?</a></li>
|
||||
<li><a href="#delold">What happens if I turn on “Delete Messages from Device”?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
@@ -218,19 +218,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="podporuje-delta-chat-obrázky-videá-a-iné-prílohy">
|
||||
|
||||
|
||||
Podporuje Delta Chat obrázky, videá a iné prílohy? <a href="#podporuje-delta-chat-obrázky-videá-a-iné-prílohy" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Yes. 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>For performance, images are optimized and sent at a smaller size by default, but you can send it as a “file” to preserve the original.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -411,6 +398,32 @@ Notifications are not sent and there is no time limit.</p>
|
||||
<p>Note, that the original message may still be received by chat members
|
||||
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1030,22 +1043,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1561,7 +1578,7 @@ See <a href="https://delta.chat/en/2023-05-22-webxdc-security">here for the full
|
||||
<li>
|
||||
<p>2023 March, <a href="https://cure53.de">Cure53</a> analyzed both the transport encryption of
|
||||
Delta Chat’s network connections and a reproducible mail server setup as
|
||||
<a href="https://delta.chat/sk/serverguide">recommended on this site</a>.
|
||||
<a href="https://delta.chat/serverguide">recommended on this site</a>.
|
||||
You can read more about the audit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">on our blog</a>
|
||||
or read the <a href="https://delta.chat/assets/blog/MER-01-report.pdf">full report here</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="#a-mbulon-delta-chat-i-figura-video-dhe-bashkëngjitje-të-tjera">A mbulon Delta Chat-i figura, video dhe bashkëngjitje të tjera?</a></li>
|
||||
<li><a href="#multiple-accounts">What are profiles? How can I switch between them?</a></li>
|
||||
<li><a href="#kush-e-sheh-profilin-tim">Kush e sheh profilin tim?</a></li>
|
||||
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#çdo-të-thotë-pika-e-gjelbër">Ç’do të thotë pika e gjelbër?</a></li>
|
||||
<li><a href="#çduan-të-thonë-shenjat-e-shfaqura-pas-mesazheve-që-dërgohen">Ç’duan të thonë shenjat e shfaqura pas mesazheve që dërgohen?</a></li>
|
||||
<li><a href="#edit">Correct typos and delete messages after sending</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">How do disappearing messages work?</a></li>
|
||||
<li><a href="#delold">Ç’ndodh, nëse aktivizoj “Fshi prej pajisjes mesazhe të vjetër”?</a></li>
|
||||
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
|
||||
@@ -218,19 +218,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="a-mbulon-delta-chat-i-figura-video-dhe-bashkëngjitje-të-tjera">
|
||||
|
||||
|
||||
A mbulon Delta Chat-i figura, video dhe bashkëngjitje të tjera? <a href="#a-mbulon-delta-chat-i-figura-video-dhe-bashkëngjitje-të-tjera" class="anchor"></a>
|
||||
|
||||
|
||||
</h3>
|
||||
|
||||
<p>Po 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>Si parazgjedhje, për funksionim më të mirë, figurat optimizohen dhe dërgohen në madhësi më të vogël, por mund ta dërgoni si një “kartelë”, që të ruhet origjinali.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -410,6 +397,32 @@ Notifications are not sent and there is no time limit.</p>
|
||||
<p>Note, that the original message may still be received by chat members
|
||||
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -1032,22 +1045,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1564,7 +1581,7 @@ Shihni <a href="https://delta.chat/en/2023-05-22-webxdc-security">këtu, për sh
|
||||
<li>
|
||||
<p>Në fillim të 2023-it, <a href="https://cure53.de">Cure53</a> analizoi qoftë fshehtëzimin
|
||||
e transporteve për lidhje rrjeti të Delta Chat-it, qoftë një formësim të riprodhueshëm
|
||||
shërbyesi poste si <a href="https://delta.chat/sq/serverguide">të rekomanduarin në këtë sajt</a>.
|
||||
shërbyesi poste si <a href="https://delta.chat/serverguide">të rekomanduarin në këtë sajt</a>.
|
||||
Mund të lexoni më tepër rreth auditimit <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">në blogun tonë</a>,
|
||||
ose të lexoni <a href="https://delta.chat/assets/blog/MER-01-report.pdf">raportin e plotë këtu</a>.</p>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<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="#чи-підтримує-delta-chat-вкладення-у-вигляді-фото-відео-тощо">Чи підтримує Delta Chat вкладення у вигляді фото, відео тощо?</a></li>
|
||||
<li><a href="#multiple-accounts">Що таке профілі? Як я можу перемикатися між ними?</a></li>
|
||||
<li><a href="#хто-бачить-моє-зображення-профілю">Хто бачить моє зображення профілю?</a></li>
|
||||
<li><a href="#signature">Чи можу я встановити біографію/статус у Delta Chat?</a></li>
|
||||
@@ -14,6 +13,7 @@
|
||||
<li><a href="#що-означає-зелена-точка">Що означає зелена точка?</a></li>
|
||||
<li><a href="#що-означають-галочки-біля-вихідних-повідомлень">Що означають галочки біля вихідних повідомлень?</a></li>
|
||||
<li><a href="#edit">Виправлення помилок та видалення повідомлень після надсилання</a></li>
|
||||
<li><a href="#mediaquality">How is media quality handled?</a></li>
|
||||
<li><a href="#ephemeralmsgs">Як працюють повідомлення, що зникають?</a></li>
|
||||
<li><a href="#delold">Що станеться, якщо я ввімкну «Видаляти старі повідомлення з пристрою»?</a></li>
|
||||
</ul>
|
||||
@@ -204,19 +204,6 @@ You can also add a little introduction message.</p>
|
||||
<p>The second contact will receive a <strong>card</strong> then
|
||||
and can tap it to start chatting with the first contact.</p>
|
||||
|
||||
<h3 id="чи-підтримує-delta-chat-вкладення-у-вигляді-фото-відео-тощо">
|
||||
|
||||
|
||||
Чи підтримує Delta Chat вкладення у вигляді фото, відео тощо? <a href="#чи-підтримує-delta-chat-вкладення-у-вигляді-фото-відео-тощо" 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>Attachment-</strong>
|
||||
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
|
||||
|
||||
<p>З міркувань продуктивності, зображення оптимізовані та надсилаються в меншому розмірі за замовчуванням, але ви можете надіслати їх як «файл», щоб зберегти оригінал.</p>
|
||||
|
||||
<h3 id="multiple-accounts">
|
||||
|
||||
|
||||
@@ -381,6 +368,32 @@ has <strong>Settings → Chats → Read Receipts</strong> enabled.</p>
|
||||
|
||||
<p>Зауважте, що початкове повідомлення все ще може бути отримане учасниками чату які могли вже відповісти, переслати, зберегти, зробити знімок екрану або іншим чином скопіювати повідомлення.</p>
|
||||
|
||||
<h3 id="mediaquality">
|
||||
|
||||
|
||||
How is media quality handled? <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>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>In regions with worse connectivity,
|
||||
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="ephemeralmsgs">
|
||||
|
||||
|
||||
@@ -932,22 +945,26 @@ Relays are operated by different groups and people.</p>
|
||||
<p>By default, after installation, a relay is <strong>automatically set up</strong>,
|
||||
so you do not need to care about that.
|
||||
However, if you want to,
|
||||
you can configure relays at At <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
you can configure relays at <strong>Settings → Advanced → Relays</strong>:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<p>You can <strong>add</strong> a relay by scanning its QR code;
|
||||
<a href="https://chatmail.at/relays">https://chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.</p>
|
||||
<a href="https://chatmail.at/relays">chatmail.at/relays</a> shows some known ones.
|
||||
If you have multiple relays, you will receive messages on all of them.
|
||||
Contacts learn your current relays automatically when you message them.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>The <strong>default</strong> defines the one where your chat partners send future messages to.</p>
|
||||
<p>Tap on a relay to set it as <strong>used for sending</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>If you <strong>remove</strong> a relay,
|
||||
make sure another default relay was used for a sufficient amount of time.
|
||||
Otherwise, messages from your chat partners won’t reach you.
|
||||
If in doubt, remove later.</p>
|
||||
contacts who only know this relay may not reach you until you message them again.
|
||||
To stay reachable in the meantime, choose <strong>Hide from Contacts</strong> in the confirmation dialog
|
||||
instead of removing it right away.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To <strong>show</strong> a hidden relay again, tap on it.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1374,7 +1391,7 @@ you need to look it up in the “keypairs” SQLite table of a profile backup ta
|
||||
<p>Починаючи з 2023 року, ми виправили проблеми з безпекою та конфіденційністю у функції “веб застосунків, що поширені у чаті”, пов’язані зі збоями в роботі пісочниці особливо в Chromium. Згодом ми отримали незалежний аудит безпеки аудит безпеки від Cure53, і всі знайдені проблеми були виправлені в серії додатків 1.36, випущених у квітні 2023 року. Повну історію про наскрізну безпеку в Інтернеті дивіться <a href="https://delta.chat/en/2023-05-22-webxdc-security">тут</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Починаючи з 2023 року <a href="https://cure53.de">Cure53</a> проаналізував транспортне шифрування мережевих з’єднань Delta Chat і відтворюване налаштування поштового сервера як <a href="https://delta.chat/uk/serverguide">рекомендовано на цьому сайті</a>. Ви можете прочитати більше про аудит <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">у нашому блозі</a> або прочитайте <a href="https://delta.chat/assets/blog/MER-01-report.pdf">повний звіт тут</a>.</p>
|
||||
<p>Починаючи з 2023 року <a href="https://cure53.de">Cure53</a> проаналізував транспортне шифрування мережевих з’єднань Delta Chat і відтворюване налаштування поштового сервера як <a href="https://delta.chat/serverguide">рекомендовано на цьому сайті</a>. Ви можете прочитати більше про аудит <a href="https://delta.chat/en/2023-03-27-third-independent-security-audit">у нашому блозі</a> або прочитайте <a href="https://delta.chat/assets/blog/MER-01-report.pdf">повний звіт тут</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>У 2020 році <a href="https://includesecurity.com">Include Security</a> проаналізувала Rust-<a href="https://github.com/deltachat/deltachat-core-rust/">ядро</a> Delta Chat і бібліотеки <a href="https://github.com/async-email/async-imap">IMAP</a>, <a href="https://github.com/async-email/async-smtp">SMTP</a> та <a href="https://github.com/async-email/async-native-tls">TLS</a>. Він не виявив критичних або серйозних проблем. У звіті виявлено кілька слабких місць середнього ступеня тяжкості – вони самі по собі не становлять загрози для користувачів Delta Chat оскільки вони залежать від середовища, у якому використовується Delta Chat. З міркувань зручності використання та сумісності ми не можемо пом’якшити їх усі тому вирішили надати рекомендації щодо безпеки користувачам, яким загрожує небезпека. Ви можете прочитати <a href="https://delta.chat/assets/blog/2020-second-security-review.pdf">повний звіт тут</a>.</p>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -14,7 +14,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/* Basic RPC Transport implementation */
|
||||
public abstract class BaseTransport implements Rpc.Transport {
|
||||
public abstract class BaseRpcTransport implements Rpc.RpcTransport {
|
||||
private final Map<Integer, SettableFuture<JsonNode>> requestFutures = new ConcurrentHashMap<>();
|
||||
private int requestId = 0;
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
@@ -9,16 +9,16 @@ import chat.delta.rpc.types.*;
|
||||
|
||||
public class Rpc {
|
||||
|
||||
public interface Transport {
|
||||
public interface RpcTransport {
|
||||
void call(String method, JsonNode... params) throws RpcException;
|
||||
<T> T callForResult(TypeReference<T> resultType, String method, JsonNode... params) throws RpcException;
|
||||
ObjectMapper getObjectMapper();
|
||||
}
|
||||
|
||||
public final Transport transport;
|
||||
public final RpcTransport transport;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public Rpc(Transport transport) {
|
||||
public Rpc(RpcTransport transport) {
|
||||
this.transport = transport;
|
||||
this.mapper = transport.getObjectMapper();
|
||||
}
|
||||
@@ -289,6 +289,7 @@ public class Rpc {
|
||||
* from a server encoded in a QR code.
|
||||
* - [Self::list_transports()] to get a list of all configured transports.
|
||||
* - [Self::delete_transport()] to remove a transport.
|
||||
* - [Self::set_transport_unpublished()] to set whether contacts see this transport.
|
||||
*/
|
||||
public void addOrUpdateTransport(Integer accountId, EnteredLoginParam param) throws RpcException {
|
||||
transport.call("add_or_update_transport", mapper.valueToTree(accountId), mapper.valueToTree(param));
|
||||
@@ -312,11 +313,22 @@ public class Rpc {
|
||||
* Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
* Use [Self::add_or_update_transport()] to add or change a transport
|
||||
* and [Self::delete_transport()] to delete a transport.
|
||||
* Use [Self::list_transports_ex()] to additionally query
|
||||
* whether the transports are marked as 'unpublished'.
|
||||
*/
|
||||
public java.util.List<EnteredLoginParam> listTransports(Integer accountId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<java.util.List<EnteredLoginParam>>(){}, "list_transports", mapper.valueToTree(accountId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
* Use [Self::add_or_update_transport()] to add or change a transport
|
||||
* and [Self::delete_transport()] to delete a transport.
|
||||
*/
|
||||
public java.util.List<TransportListEntry> listTransportsEx(Integer accountId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<java.util.List<TransportListEntry>>(){}, "list_transports_ex", mapper.valueToTree(accountId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the transport with the specified email address
|
||||
* (i.e. [EnteredLoginParam::addr]).
|
||||
@@ -325,6 +337,22 @@ public class Rpc {
|
||||
transport.call("delete_transport", mapper.valueToTree(accountId), mapper.valueToTree(addr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change whether the transport is unpublished.
|
||||
* <p>
|
||||
* Unpublished transports are not advertised to contacts,
|
||||
* and self-sent messages are not sent there,
|
||||
* so that we don't cause extra messages to the corresponding inbox,
|
||||
* but can still receive messages from contacts who don't know our new transport addresses yet.
|
||||
* <p>
|
||||
* The default is false, but when the user updates from a version that didn't have this flag,
|
||||
* existing secondary transports are set to unpublished,
|
||||
* so that an existing transport address doesn't suddenly get spammed with a lot of messages.
|
||||
*/
|
||||
public void setTransportUnpublished(Integer accountId, String addr, Boolean unpublished) throws RpcException {
|
||||
transport.call("set_transport_unpublished", mapper.valueToTree(accountId), mapper.valueToTree(addr), mapper.valueToTree(unpublished));
|
||||
}
|
||||
|
||||
/** Signal an ongoing process to stop. */
|
||||
public void stopOngoingProcess(Integer accountId) throws RpcException {
|
||||
transport.call("stop_ongoing_process", mapper.valueToTree(accountId));
|
||||
@@ -368,7 +396,7 @@ public class Rpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets messages to be processed by the bot and returns their IDs.
|
||||
* (deprecated) Gets messages to be processed by the bot and returns their IDs.
|
||||
* <p>
|
||||
* Only messages with database ID higher than `last_msg_id` config value
|
||||
* are returned. After processing the messages, the bot should
|
||||
@@ -376,6 +404,13 @@ public class Rpc {
|
||||
* or manually updating the value to avoid getting already
|
||||
* processed messages.
|
||||
* <p>
|
||||
* Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
||||
* even if it is not fully downloaded yet.
|
||||
* The bot needs to wait for the message to be fully downloaded.
|
||||
* Since this is usually not the desired behavior,
|
||||
* bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
|
||||
* event for getting notified about new messages.
|
||||
* <p>
|
||||
* [`markseen_msgs`]: Self::markseen_msgs
|
||||
*/
|
||||
public java.util.List<Integer> getNextMsgs(Integer accountId) throws RpcException {
|
||||
@@ -383,7 +418,7 @@ public class Rpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for messages to be processed by the bot and returns their IDs.
|
||||
* (deprecated) Waits for messages to be processed by the bot and returns their IDs.
|
||||
* <p>
|
||||
* This function is similar to [`get_next_msgs`],
|
||||
* but waits for internal new message notification before returning.
|
||||
@@ -394,6 +429,13 @@ public class Rpc {
|
||||
* To shutdown the bot, stopping I/O can be used to interrupt
|
||||
* pending or next `wait_next_msgs` call.
|
||||
* <p>
|
||||
* Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
||||
* even if it is not fully downloaded yet.
|
||||
* The bot needs to wait for the message to be fully downloaded.
|
||||
* Since this is usually not the desired behavior,
|
||||
* bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
|
||||
* event for getting notified about new messages.
|
||||
* <p>
|
||||
* [`get_next_msgs`]: Self::get_next_msgs
|
||||
*/
|
||||
public java.util.List<Integer> waitNextMsgs(Integer accountId) throws RpcException {
|
||||
@@ -410,14 +452,6 @@ public class Rpc {
|
||||
return transport.callForResult(new TypeReference<Integer>(){}, "estimate_auto_deletion_count", mapper.valueToTree(accountId), mapper.valueToTree(fromServer), mapper.valueToTree(seconds));
|
||||
}
|
||||
|
||||
public String initiateAutocryptKeyTransfer(Integer accountId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<String>(){}, "initiate_autocrypt_key_transfer", mapper.valueToTree(accountId));
|
||||
}
|
||||
|
||||
public void continueAutocryptKeyTransfer(Integer accountId, Integer messageId, String setupCode) throws RpcException {
|
||||
transport.call("continue_autocrypt_key_transfer", mapper.valueToTree(accountId), mapper.valueToTree(messageId), mapper.valueToTree(setupCode));
|
||||
}
|
||||
|
||||
public java.util.List<Integer> getChatlistEntries(Integer accountId, Integer listFlags, String queryString, Integer queryContactId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<java.util.List<Integer>>(){}, "get_chatlist_entries", mapper.valueToTree(accountId), mapper.valueToTree(listFlags), mapper.valueToTree(queryString), mapper.valueToTree(queryContactId));
|
||||
}
|
||||
@@ -509,6 +543,8 @@ public class Rpc {
|
||||
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||
* an out-of-band-verification can be joined using `secure_join()`
|
||||
* <p>
|
||||
* @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
|
||||
* <p>
|
||||
* chat_id: If set to a group-chat-id,
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for protected groups as well as for normal groups.
|
||||
@@ -798,6 +834,16 @@ public class Rpc {
|
||||
transport.call("marknoticed_chat", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the last incoming message in the chat as _fresh_.
|
||||
* <p>
|
||||
* UI can use this to offer a "mark unread" option,
|
||||
* so that already noticed chats get a badge counter again.
|
||||
*/
|
||||
public void markfreshChat(Integer accountId, Integer chatId) throws RpcException {
|
||||
transport.call("markfresh_chat", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message that is immediately followed by the last seen
|
||||
* message.
|
||||
@@ -1211,12 +1257,19 @@ public class Rpc {
|
||||
* even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
* but will fail after 60 seconds to avoid deadlocks.
|
||||
* <p>
|
||||
* @deprecated as of 2026-03; use `create_qr_svg(get_backup_qr())` instead.
|
||||
* <p>
|
||||
* Returns the QR code rendered as an SVG image.
|
||||
*/
|
||||
public String getBackupQrSvg(Integer accountId) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<String>(){}, "get_backup_qr_svg", mapper.valueToTree(accountId));
|
||||
}
|
||||
|
||||
/** Renders the given text as a QR code SVG image. */
|
||||
public String createQrSvg(String text) throws RpcException {
|
||||
return transport.callForResult(new TypeReference<String>(){}, "create_qr_svg", mapper.valueToTree(text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a backup from a remote provider.
|
||||
* <p>
|
||||
|
||||
@@ -387,6 +387,8 @@ public abstract class EventType {
|
||||
public static class IncomingCallAccepted extends EventType {
|
||||
/** ID of the chat which the message belongs to. */
|
||||
public Integer chat_id;
|
||||
/** The call was accepted from this device (process). */
|
||||
public Boolean from_this_device;
|
||||
/** ID of the info message referring to the call. */
|
||||
public Integer msg_id;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ public class Message {
|
||||
public Boolean isEdited;
|
||||
public Boolean isForwarded;
|
||||
public Boolean isInfo;
|
||||
public Boolean isSetupmessage;
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public Integer originalMsgId;
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
@@ -47,8 +46,6 @@ public class Message {
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public Integer savedMessageId;
|
||||
public Contact sender;
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public String setupCodeBegin;
|
||||
/**
|
||||
* True if the message was correctly encrypted&signed, false otherwise. Historically, UIs showed a small padlock on the message then.
|
||||
* <p>
|
||||
|
||||
@@ -42,7 +42,6 @@ public abstract class MessageLoadResult {
|
||||
public Boolean isEdited;
|
||||
public Boolean isForwarded;
|
||||
public Boolean isInfo;
|
||||
public Boolean isSetupmessage;
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public Integer originalMsgId;
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
@@ -57,8 +56,6 @@ public abstract class MessageLoadResult {
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public Integer savedMessageId;
|
||||
public Contact sender;
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
|
||||
public String setupCodeBegin;
|
||||
/**
|
||||
* True if the message was correctly encrypted&signed, false otherwise. Historically, UIs showed a small padlock on the message then.
|
||||
* <p>
|
||||
|
||||
@@ -25,6 +25,8 @@ public abstract class Qr {
|
||||
public String fingerprint;
|
||||
/** Invite number. */
|
||||
public String invitenumber;
|
||||
/** Whether the inviter supports the new Securejoin v3 protocol */
|
||||
public Boolean is_v3;
|
||||
}
|
||||
|
||||
/** Ask the user whether to join the group. */
|
||||
@@ -41,6 +43,8 @@ public abstract class Qr {
|
||||
public String grpname;
|
||||
/** Invite number. */
|
||||
public String invitenumber;
|
||||
/** Whether the inviter supports the new Securejoin v3 protocol */
|
||||
public Boolean is_v3;
|
||||
}
|
||||
|
||||
/** Ask the user whether to join the broadcast channel. */
|
||||
@@ -55,6 +59,8 @@ public abstract class Qr {
|
||||
public String grpid;
|
||||
/** Invite number. */
|
||||
public String invitenumber;
|
||||
/** Whether the inviter supports the new Securejoin v3 protocol */
|
||||
public Boolean is_v3;
|
||||
/** The user-visible name of this broadcast channel */
|
||||
public String name;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Autogenerated file, do not edit manually */
|
||||
package chat.delta.rpc.types;
|
||||
|
||||
public class TransportListEntry {
|
||||
/** Whether this transport is set to 'unpublished'. See `set_transport_unpublished` / `setTransportUnpublished` for details. */
|
||||
public Boolean isUnpublished;
|
||||
/** The login data entered by the user. */
|
||||
public EnteredLoginParam param;
|
||||
}
|
||||
@@ -176,8 +176,6 @@ public class DcContext {
|
||||
|
||||
public native String getConnectivityHtml();
|
||||
|
||||
public native String initiateKeyTransfer();
|
||||
|
||||
public native void imex(int what, String dir);
|
||||
|
||||
public native String imexHasBackup(String dir);
|
||||
|
||||
@@ -25,7 +25,6 @@ public class DcMsg {
|
||||
public static final int DC_INFO_GROUP_IMAGE_CHANGED = 3;
|
||||
public static final int DC_INFO_MEMBER_ADDED_TO_GROUP = 4;
|
||||
public static final int DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5;
|
||||
public static final int DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6;
|
||||
public static final int DC_INFO_SECURE_JOIN_MESSAGE = 7;
|
||||
public static final int DC_INFO_LOCATIONSTREAMING_ENABLED = 8;
|
||||
public static final int DC_INFO_LOCATION_ONLY = 9;
|
||||
@@ -191,8 +190,6 @@ public class DcMsg {
|
||||
|
||||
public native boolean hasHtml();
|
||||
|
||||
public native String getSetupCodeBegin();
|
||||
|
||||
public native void setText(String text);
|
||||
|
||||
public native void setSubject(String text);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.b44t.messenger;
|
||||
|
||||
import chat.delta.rpc.BaseTransport;
|
||||
import chat.delta.rpc.BaseRpcTransport;
|
||||
|
||||
/* RPC transport over C FFI */
|
||||
public class FFITransport extends BaseTransport {
|
||||
public class FFITransport extends BaseRpcTransport {
|
||||
private final DcJsonrpcInstance dcJsonrpcInstance;
|
||||
|
||||
public FFITransport(DcJsonrpcInstance dcJsonrpcInstance) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.net.LinkProperties;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
@@ -19,14 +18,19 @@ import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import com.b44t.messenger.DcAccounts;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcEventChannel;
|
||||
import com.b44t.messenger.DcEventEmitter;
|
||||
import com.b44t.messenger.FFITransport;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.thoughtcrime.securesms.calls.CallCoordinator;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -48,35 +52,27 @@ import org.thoughtcrime.securesms.util.SignalProtocolLoggerProvider;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.webxdc.WebxdcGarbageCollectionWorker;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class ApplicationContext extends MultiDexApplication {
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
private static final Object initLock = new Object();
|
||||
private static volatile boolean isInitialized = false;
|
||||
|
||||
private static DcAccounts dcAccounts;
|
||||
private Rpc rpc;
|
||||
private DcContext dcContext;
|
||||
private static DcAccounts dcAccounts;
|
||||
private Rpc rpc;
|
||||
private DcContext dcContext;
|
||||
|
||||
private DcLocationManager dcLocationManager;
|
||||
private DcEventCenter eventCenter;
|
||||
private NotificationCenter notificationCenter;
|
||||
private JobManager jobManager;
|
||||
private DcLocationManager dcLocationManager;
|
||||
private DcEventCenter eventCenter;
|
||||
private NotificationCenter notificationCenter;
|
||||
private JobManager jobManager;
|
||||
|
||||
private int debugOnAvailableCount;
|
||||
private int debugOnBlockedStatusChangedCount;
|
||||
private int debugOnCapabilitiesChangedCount;
|
||||
private int debugOnLinkPropertiesChangedCount;
|
||||
private int debugOnAvailableCount;
|
||||
private int debugOnBlockedStatusChangedCount;
|
||||
private int debugOnCapabilitiesChangedCount;
|
||||
private int debugOnLinkPropertiesChangedCount;
|
||||
|
||||
public static ApplicationContext getInstance(@NonNull Context context) {
|
||||
return (ApplicationContext)context.getApplicationContext();
|
||||
return (ApplicationContext) context.getApplicationContext();
|
||||
}
|
||||
|
||||
private static void ensureInitialized() {
|
||||
@@ -93,8 +89,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DcAccounts instance, waiting for initialization if necessary.
|
||||
* This method is thread-safe and will block until initialization is complete.
|
||||
* Get DcAccounts instance, waiting for initialization if necessary. This method is thread-safe
|
||||
* and will block until initialization is complete.
|
||||
*/
|
||||
public static DcAccounts getDcAccounts() {
|
||||
ensureInitialized();
|
||||
@@ -102,8 +98,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Rpc instance, waiting for initialization if necessary.
|
||||
* This method is thread-safe and will block until initialization is complete.
|
||||
* Get Rpc instance, waiting for initialization if necessary. This method is thread-safe and will
|
||||
* block until initialization is complete.
|
||||
*/
|
||||
public Rpc getRpc() {
|
||||
ensureInitialized();
|
||||
@@ -111,8 +107,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DcContext instance, waiting for initialization if necessary.
|
||||
* This method is thread-safe and will block until initialization is complete.
|
||||
* Get DcContext instance, waiting for initialization if necessary. This method is thread-safe and
|
||||
* will block until initialization is complete.
|
||||
*/
|
||||
public DcContext getDcContext() {
|
||||
ensureInitialized();
|
||||
@@ -121,8 +117,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
|
||||
/**
|
||||
* Set DcContext instance. This should only be called by AccountManager when switching accounts,
|
||||
* which only happens after initial initialization is complete.
|
||||
* This method is thread-safe but does NOT trigger initialization or notify waiting threads.
|
||||
* which only happens after initial initialization is complete. This method is thread-safe but
|
||||
* does NOT trigger initialization or notify waiting threads.
|
||||
*/
|
||||
public void setDcContext(DcContext dcContext) {
|
||||
synchronized (initLock) {
|
||||
@@ -131,8 +127,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DcLocationManager instance, waiting for initialization if necessary.
|
||||
* This method is thread-safe and will block until initialization is complete.
|
||||
* Get DcLocationManager instance, waiting for initialization if necessary. This method is
|
||||
* thread-safe and will block until initialization is complete.
|
||||
*/
|
||||
public DcLocationManager getLocationManager() {
|
||||
ensureInitialized();
|
||||
@@ -140,8 +136,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DcEventCenter instance, waiting for initialization if necessary.
|
||||
* This method is thread-safe and will block until initialization is complete.
|
||||
* Get DcEventCenter instance, waiting for initialization if necessary. This method is thread-safe
|
||||
* and will block until initialization is complete.
|
||||
*/
|
||||
public DcEventCenter getEventCenter() {
|
||||
ensureInitialized();
|
||||
@@ -149,8 +145,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NotificationCenter instance, waiting for initialization if necessary.
|
||||
* This method is thread-safe and will block until initialization is complete.
|
||||
* Get NotificationCenter instance, waiting for initialization if necessary. This method is
|
||||
* thread-safe and will block until initialization is complete.
|
||||
*/
|
||||
public NotificationCenter getNotificationCenter() {
|
||||
ensureInitialized();
|
||||
@@ -161,26 +157,30 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
throwable.printStackTrace(new PrintWriter(stringWriter, true));
|
||||
String errorMsg = "Android " + Build.VERSION.RELEASE +":\n" + stringWriter.getBuffer().toString();
|
||||
errorMsg += "\n" + LogViewFragment.grabLogcat();
|
||||
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
|
||||
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
|
||||
intent.putExtra(android.content.Intent.EXTRA_TEXT, subject + "\n\n" + errorMsg);
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, "adb@merlinux.eu");
|
||||
Intent chooser = Intent.createChooser(intent, subject);
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
|
||||
startActivity(chooser);
|
||||
Thread.setDefaultUncaughtExceptionHandler(
|
||||
(thread, throwable) -> {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
throwable.printStackTrace(new PrintWriter(stringWriter, true));
|
||||
String errorMsg =
|
||||
"Android " + Build.VERSION.RELEASE + ":\n" + stringWriter.getBuffer().toString();
|
||||
errorMsg += "\n" + LogViewFragment.grabLogcat();
|
||||
String subject =
|
||||
"ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
|
||||
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
|
||||
intent.putExtra(android.content.Intent.EXTRA_TEXT, subject + "\n\n" + errorMsg);
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, "adb@merlinux.eu");
|
||||
Intent chooser = Intent.createChooser(intent, subject);
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
|
||||
startActivity(chooser);
|
||||
|
||||
try {
|
||||
ApplicationContext.this.finalize();
|
||||
} catch (Throwable e) {}
|
||||
});
|
||||
try {
|
||||
ApplicationContext.this.finalize();
|
||||
} catch (Throwable e) {
|
||||
}
|
||||
});
|
||||
|
||||
// if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||
// // This process is dedicated to LeakCanary for heap analysis.
|
||||
@@ -194,90 +194,114 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
System.loadLibrary("native-utils");
|
||||
|
||||
// Initialize DcAccounts in background to avoid ANR during SQL migrations
|
||||
Util.runOnBackground(() -> {
|
||||
synchronized (initLock) {
|
||||
try {
|
||||
DcEventChannel eventChannel = new DcEventChannel();
|
||||
DcEventEmitter emitter = eventChannel.getEventEmitter();
|
||||
eventCenter = new DcEventCenter(this);
|
||||
|
||||
new Thread(() -> {
|
||||
Log.i(TAG, "Starting event loop");
|
||||
while (true) {
|
||||
DcEvent event = emitter.getNextEvent();
|
||||
if (event == null) {
|
||||
break;
|
||||
}
|
||||
if (isInitialized) {
|
||||
eventCenter.handleEvent(event);
|
||||
} else {
|
||||
// not fully initialized, only handle logging events,
|
||||
// ex. account migrations during DcAccounts initialization
|
||||
eventCenter.handleLogging(event);
|
||||
}
|
||||
}
|
||||
Log.i("DeltaChat", "shutting down event handler");
|
||||
}, "eventThread").start();
|
||||
|
||||
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel);
|
||||
Log.i(TAG, "DcAccounts created");
|
||||
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
|
||||
Log.i(TAG, "Rpc created");
|
||||
AccountManager.getInstance().migrateToDcAccounts(this);
|
||||
|
||||
int[] allAccounts = dcAccounts.getAll();
|
||||
Log.i(TAG, "Number of profiles: " + allAccounts.length);
|
||||
for (int accountId : allAccounts) {
|
||||
DcContext ac = dcAccounts.getAccount(accountId);
|
||||
if (!ac.isOpen()) {
|
||||
try {
|
||||
DatabaseSecret secret = DatabaseSecretProvider.getOrCreateDatabaseSecret(this, accountId);
|
||||
boolean res = ac.open(secret.asString());
|
||||
if (res) Log.i(TAG, "Successfully opened account " + accountId + ", path: " + ac.getBlobdir());
|
||||
else Log.e(TAG, "Error opening account " + accountId + ", path: " + ac.getBlobdir());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to open account " + accountId + ", path: " + ac.getBlobdir() + ": " + e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
Util.runOnBackground(
|
||||
() -> {
|
||||
synchronized (initLock) {
|
||||
try {
|
||||
rpc.addAccount();
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
DcEventChannel eventChannel = new DcEventChannel();
|
||||
DcEventEmitter emitter = eventChannel.getEventEmitter();
|
||||
eventCenter = new DcEventCenter(this);
|
||||
|
||||
new Thread(
|
||||
() -> {
|
||||
Log.i(TAG, "Starting event loop");
|
||||
while (true) {
|
||||
DcEvent event = emitter.getNextEvent();
|
||||
if (event == null) {
|
||||
break;
|
||||
}
|
||||
if (isInitialized) {
|
||||
eventCenter.handleEvent(event);
|
||||
} else {
|
||||
// not fully initialized, only handle logging events,
|
||||
// ex. account migrations during DcAccounts initialization
|
||||
eventCenter.handleLogging(event);
|
||||
}
|
||||
}
|
||||
Log.i("DeltaChat", "shutting down event handler");
|
||||
},
|
||||
"eventThread")
|
||||
.start();
|
||||
|
||||
dcAccounts =
|
||||
new DcAccounts(
|
||||
new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel);
|
||||
Log.i(TAG, "DcAccounts created");
|
||||
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
|
||||
Log.i(TAG, "Rpc created");
|
||||
AccountManager.getInstance().migrateToDcAccounts(this);
|
||||
|
||||
int[] allAccounts = dcAccounts.getAll();
|
||||
Log.i(TAG, "Number of profiles: " + allAccounts.length);
|
||||
for (int accountId : allAccounts) {
|
||||
DcContext ac = dcAccounts.getAccount(accountId);
|
||||
if (!ac.isOpen()) {
|
||||
try {
|
||||
DatabaseSecret secret =
|
||||
DatabaseSecretProvider.getOrCreateDatabaseSecret(this, accountId);
|
||||
boolean res = ac.open(secret.asString());
|
||||
if (res)
|
||||
Log.i(
|
||||
TAG,
|
||||
"Successfully opened account "
|
||||
+ accountId
|
||||
+ ", path: "
|
||||
+ ac.getBlobdir());
|
||||
else
|
||||
Log.e(
|
||||
TAG, "Error opening account " + accountId + ", path: " + ac.getBlobdir());
|
||||
} catch (Exception e) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Failed to open account "
|
||||
+ accountId
|
||||
+ ", path: "
|
||||
+ ac.getBlobdir()
|
||||
+ ": "
|
||||
+ e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
rpc.addAccount();
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
dcContext = dcAccounts.getSelectedAccount();
|
||||
notificationCenter = new NotificationCenter(this);
|
||||
dcLocationManager = new DcLocationManager(this, dcContext);
|
||||
|
||||
isInitialized = true;
|
||||
initLock.notifyAll();
|
||||
Log.i(TAG, "DcAccounts initialization complete");
|
||||
|
||||
// set translations before starting I/O to avoid sending untranslated MDNs (issue
|
||||
// #2288)
|
||||
DcHelper.setStockTranslations(this);
|
||||
|
||||
dcAccounts.startIo();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Fatal error during DcAccounts initialization", e);
|
||||
// Mark as initialized even on error to avoid deadlock
|
||||
isInitialized = true;
|
||||
initLock.notifyAll();
|
||||
throw new RuntimeException("Failed to initialize DcAccounts", e);
|
||||
}
|
||||
}
|
||||
dcContext = dcAccounts.getSelectedAccount();
|
||||
notificationCenter = new NotificationCenter(this);
|
||||
dcLocationManager = new DcLocationManager(this, dcContext);
|
||||
|
||||
isInitialized = true;
|
||||
initLock.notifyAll();
|
||||
Log.i(TAG, "DcAccounts initialization complete");
|
||||
|
||||
// set translations before starting I/O to avoid sending untranslated MDNs (issue #2288)
|
||||
DcHelper.setStockTranslations(this);
|
||||
|
||||
dcAccounts.startIo();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Fatal error during DcAccounts initialization", e);
|
||||
// Mark as initialized even on error to avoid deadlock
|
||||
isInitialized = true;
|
||||
initLock.notifyAll();
|
||||
throw new RuntimeException("Failed to initialize DcAccounts", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// October-2025 migration: delete deprecated "permanent channel" id
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
@@ -288,33 +312,50 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
ConnectivityManager connectivityManager =
|
||||
(ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
connectivityManager.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() {
|
||||
@Override
|
||||
public void onAvailable(@NonNull android.net.Network network) {
|
||||
Log.i("DeltaChat", "++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
|
||||
getDcAccounts().maybeNetwork();
|
||||
}
|
||||
(ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
connectivityManager.registerDefaultNetworkCallback(
|
||||
new ConnectivityManager.NetworkCallback() {
|
||||
@Override
|
||||
public void onAvailable(@NonNull android.net.Network network) {
|
||||
Log.i(
|
||||
"DeltaChat",
|
||||
"++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
|
||||
getDcAccounts().maybeNetwork();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockedStatusChanged(@NonNull android.net.Network network, boolean blocked) {
|
||||
Log.i("DeltaChat", "++++++++++++++++++ NetworkCallback.onBlockedStatusChanged() #" + debugOnBlockedStatusChangedCount++);
|
||||
}
|
||||
@Override
|
||||
public void onBlockedStatusChanged(
|
||||
@NonNull android.net.Network network, boolean blocked) {
|
||||
Log.i(
|
||||
"DeltaChat",
|
||||
"++++++++++++++++++ NetworkCallback.onBlockedStatusChanged() #"
|
||||
+ debugOnBlockedStatusChangedCount++);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCapabilitiesChanged(@NonNull android.net.Network network, NetworkCapabilities networkCapabilities) {
|
||||
// usually called after onAvailable(), so a maybeNetwork seems contraproductive
|
||||
Log.i("DeltaChat", "++++++++++++++++++ NetworkCallback.onCapabilitiesChanged() #" + debugOnCapabilitiesChangedCount++);
|
||||
}
|
||||
@Override
|
||||
public void onCapabilitiesChanged(
|
||||
@NonNull android.net.Network network, NetworkCapabilities networkCapabilities) {
|
||||
// usually called after onAvailable(), so a maybeNetwork seems contraproductive
|
||||
Log.i(
|
||||
"DeltaChat",
|
||||
"++++++++++++++++++ NetworkCallback.onCapabilitiesChanged() #"
|
||||
+ debugOnCapabilitiesChangedCount++);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLinkPropertiesChanged(@NonNull android.net.Network network, LinkProperties linkProperties) {
|
||||
Log.i("DeltaChat", "++++++++++++++++++ NetworkCallback.onLinkPropertiesChanged() #" + debugOnLinkPropertiesChangedCount++);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onLinkPropertiesChanged(
|
||||
@NonNull android.net.Network network, LinkProperties linkProperties) {
|
||||
Log.i(
|
||||
"DeltaChat",
|
||||
"++++++++++++++++++ NetworkCallback.onLinkPropertiesChanged() #"
|
||||
+ debugOnLinkPropertiesChangedCount++);
|
||||
}
|
||||
});
|
||||
} // no else: use old method for debugging
|
||||
BroadcastReceiver networkStateReceiver = new NetworkStateReceiver();
|
||||
registerReceiver(networkStateReceiver, new IntentFilter(android.net.ConnectivityManager.CONNECTIVITY_ACTION));
|
||||
registerReceiver(
|
||||
networkStateReceiver,
|
||||
new IntentFilter(android.net.ConnectivityManager.CONNECTIVITY_ACTION));
|
||||
|
||||
KeepAliveService.maybeStartSelf(this);
|
||||
|
||||
@@ -322,16 +363,23 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
initializeJobManager();
|
||||
InChatSounds.getInstance(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
EglUtils.getEglBase();
|
||||
CallCoordinator.getInstance(this);
|
||||
}
|
||||
|
||||
DynamicTheme.setDefaultDayNightMode(this);
|
||||
|
||||
IntentFilter filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
|
||||
registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
registerReceiver(
|
||||
new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Util.localeChanged();
|
||||
DcHelper.setStockTranslations(context);
|
||||
}
|
||||
}, filter);
|
||||
}
|
||||
},
|
||||
filter);
|
||||
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
|
||||
@@ -342,31 +390,34 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
// MAYBE TODO: i think the ApplicationContext is also created
|
||||
// when the app is stated by FetchWorker timeouts.
|
||||
// in this case, the normal threads shall not be started.
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
PeriodicWorkRequest fetchWorkRequest = new PeriodicWorkRequest.Builder(
|
||||
FetchWorker.class,
|
||||
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, // usually 15 minutes
|
||||
TimeUnit.MILLISECONDS,
|
||||
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, // the start may be preferred by up to 5 minutes, so we run every 10-15 minutes
|
||||
TimeUnit.MILLISECONDS)
|
||||
Constraints constraints =
|
||||
new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
|
||||
PeriodicWorkRequest fetchWorkRequest =
|
||||
new PeriodicWorkRequest.Builder(
|
||||
FetchWorker.class,
|
||||
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, // usually 15 minutes
|
||||
TimeUnit.MILLISECONDS,
|
||||
PeriodicWorkRequest
|
||||
.MIN_PERIODIC_FLEX_MILLIS, // the start may be preferred by up to 5 minutes,
|
||||
// so we run every 10-15 minutes
|
||||
TimeUnit.MILLISECONDS)
|
||||
.setConstraints(constraints)
|
||||
.build();
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||
"FetchWorker",
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
fetchWorkRequest);
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniquePeriodicWork(
|
||||
"FetchWorker", ExistingPeriodicWorkPolicy.KEEP, fetchWorkRequest);
|
||||
}
|
||||
|
||||
PeriodicWorkRequest webxdcGarbageCollectionRequest = new PeriodicWorkRequest.Builder(
|
||||
WebxdcGarbageCollectionWorker.class,
|
||||
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
|
||||
TimeUnit.MILLISECONDS,
|
||||
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS,
|
||||
TimeUnit.MILLISECONDS)
|
||||
PeriodicWorkRequest webxdcGarbageCollectionRequest =
|
||||
new PeriodicWorkRequest.Builder(
|
||||
WebxdcGarbageCollectionWorker.class,
|
||||
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
|
||||
TimeUnit.MILLISECONDS,
|
||||
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS,
|
||||
TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniquePeriodicWork(
|
||||
"WebxdcGarbageCollectionWorker",
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
webxdcGarbageCollectionRequest);
|
||||
@@ -374,6 +425,14 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
Log.i("DeltaChat", "+++++++++++ ApplicationContext.onCreate() finished ++++++++++");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
EglUtils.release();
|
||||
}
|
||||
}
|
||||
|
||||
public JobManager getJobManager() {
|
||||
return jobManager;
|
||||
}
|
||||
|
||||
@@ -81,19 +81,14 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
public void makeSearchMenuVisible(final Menu menu, final MenuItem searchItem, boolean visible) {
|
||||
public void makeSearchMenuVisible(final Menu menu, final MenuItem searchItem) {
|
||||
for (int i = 0; i < menu.size(); ++i) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.menu_search_up || id == R.id.menu_search_down) {
|
||||
item.setVisible(visible);
|
||||
} else if (id == R.id.menu_search_counter) {
|
||||
item.setVisible(false); // always hide menu_search_counter initially
|
||||
} else if (item == searchItem) {
|
||||
; // searchItem is just always visible
|
||||
} else {
|
||||
item.setVisible(
|
||||
!visible); // if search is shown, other items are hidden - and the other way round
|
||||
item.setVisible(true);
|
||||
} else if (item != searchItem) {
|
||||
item.setVisible(false); // hide all other items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1059,7 +1059,6 @@ public class ConversationFragment extends MessageSelectorFragment {
|
||||
@Override
|
||||
public void onStickerClicked(DcMsg messageRecord) {
|
||||
new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.add_to_sticker_collection)
|
||||
.setMessage(R.string.ask_add_sticker_to_collection)
|
||||
.setPositiveButton(
|
||||
R.string.ok,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -676,7 +676,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
// should be deleted.
|
||||
try {
|
||||
DcContext dcContext = DcHelper.getContext(this);
|
||||
final String deviceMsgLabel = "update_2_33_1_android";
|
||||
final String deviceMsgLabel = "update_2_46_0a_android";
|
||||
if (!dcContext.wasDeviceMsgEverAdded(deviceMsgLabel)) {
|
||||
DcMsg msg = null;
|
||||
if (!getIntent().getBooleanExtra(FROM_WELCOME, false)) {
|
||||
@@ -688,7 +688,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
// Util.copy(inputStream, new FileOutputStream(outputFile));
|
||||
// msg.setFile(outputFile, "image/jpeg");
|
||||
|
||||
msg.setText(getString(R.string.update_2_33, "https://arcanechat.me/#contribute"));
|
||||
msg.setText(getString(R.string.update_2_46ac, "https://arcanechat.me/#contribute", "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=AM1YCd_2hKuiSiTEb-2177On&a=arcanechat%40arcanechat.me&n=ArcaneChat&b=ArcaneChat+Channel"));
|
||||
}
|
||||
dcContext.addDeviceMsg(deviceMsgLabel, msg);
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Build;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import org.webrtc.EglBase;
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
public class EglUtils {
|
||||
private static EglBase eglBase;
|
||||
|
||||
public static synchronized EglBase getEglBase() {
|
||||
if (eglBase == null) {
|
||||
eglBase = EglBase.create();
|
||||
}
|
||||
return eglBase;
|
||||
}
|
||||
|
||||
public static synchronized void release() {
|
||||
if (eglBase != null) {
|
||||
eglBase.release();
|
||||
eglBase = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,4 +257,9 @@ public class FullMsgActivity extends WebViewActivity {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldAskToOpenLink() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import java.util.Set;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class ProfileAdapter extends RecyclerView.Adapter {
|
||||
|
||||
@@ -268,7 +268,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||
accId = dcContext.getAccountId();
|
||||
}
|
||||
Intent composeIntent;
|
||||
if (accId != -1 && chatId != -1) {
|
||||
if (accId != -1 && chatId > 0) {
|
||||
composeIntent = getBaseShareIntent(ConversationActivity.class);
|
||||
composeIntent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
|
||||
composeIntent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
|
||||
|
||||
@@ -22,6 +22,8 @@ import androidx.webkit.WebSettingsCompat;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
import chat.delta.rpc.types.SecurejoinSource;
|
||||
import java.net.IDN;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.thoughtcrime.securesms.qr.QrCodeHandler;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
@@ -31,6 +33,9 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
public class WebViewActivity extends PassphraseRequiredActionBarActivity
|
||||
implements SearchView.OnQueryTextListener, WebView.FindListener {
|
||||
private static final String TAG = WebViewActivity.class.getSimpleName();
|
||||
// Regex to extract the host from a URL for IDN conversion.
|
||||
private static final Pattern URL_PATTERN =
|
||||
Pattern.compile("^((?:[a-zA-Z0-9]+://)?)([^/?#]+)(.*)$");
|
||||
|
||||
protected WebView webView;
|
||||
|
||||
@@ -194,13 +199,13 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
|
||||
public boolean onMenuItemActionExpand(final MenuItem item) {
|
||||
searchMenu = menu;
|
||||
WebViewActivity.this.lastQuery = "";
|
||||
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, true);
|
||||
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(final MenuItem item) {
|
||||
WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, false);
|
||||
WebViewActivity.this.invalidateOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -320,9 +325,17 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
if (shouldAskToOpenLink()) {
|
||||
String displayUrl;
|
||||
try {
|
||||
Matcher m = URL_PATTERN.matcher(url);
|
||||
displayUrl = m.find() ? m.group(1) + IDN.toASCII(m.group(2)) + m.group(3) : url;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// IDN.toASCII() failed (e.g. string too long), fall back to raw url
|
||||
displayUrl = url;
|
||||
}
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.open_url_confirmation)
|
||||
.setMessage(IDN.toASCII(url))
|
||||
.setMessage(displayUrl)
|
||||
.setNeutralButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.open, (d, w) -> IntentUtils.showInBrowser(this, url))
|
||||
.setNegativeButton(
|
||||
|
||||
@@ -6,24 +6,21 @@ import android.media.AudioRecord;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaMuxer;
|
||||
import android.media.MediaRecorder;
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class AudioCodec {
|
||||
|
||||
private static final String TAG = AudioCodec.class.getSimpleName();
|
||||
|
||||
private static final int SAMPLE_RATE_BALANCED = 44100;
|
||||
private static final int SAMPLE_RATE_INDEX_BALANCED = 4;
|
||||
private static final int BIT_RATE_BALANCED = 32000;
|
||||
|
||||
private static final int SAMPLE_RATE_WORSE = 24000;
|
||||
private static final int SAMPLE_RATE_INDEX_WORSE = 6;
|
||||
private static final int BIT_RATE_WORSE = 16000;
|
||||
|
||||
private static final int CHANNELS = 1;
|
||||
@@ -32,12 +29,15 @@ public class AudioCodec {
|
||||
private final MediaCodec mediaCodec;
|
||||
private final AudioRecord audioRecord;
|
||||
private final Context context;
|
||||
private final String outputPath;
|
||||
|
||||
private boolean running = true;
|
||||
private boolean finished = false;
|
||||
private volatile boolean running = true;
|
||||
private volatile boolean finished = false;
|
||||
private long startTime = 0;
|
||||
|
||||
public AudioCodec(Context context) throws IOException {
|
||||
public AudioCodec(Context context, String outputPath) throws IOException {
|
||||
this.context = context;
|
||||
this.outputPath = outputPath;
|
||||
this.bufferSize =
|
||||
AudioRecord.getMinBufferSize(
|
||||
getSampleRate(), AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
||||
@@ -49,7 +49,7 @@ public class AudioCodec {
|
||||
try {
|
||||
audioRecord.startRecording();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
Log.w(TAG, "Failed to start recording", e);
|
||||
mediaCodec.release();
|
||||
throw new IOException(e);
|
||||
}
|
||||
@@ -57,41 +57,143 @@ public class AudioCodec {
|
||||
|
||||
public synchronized void stop() {
|
||||
running = false;
|
||||
while (!finished) Util.wait(this, 0);
|
||||
while (!finished) {
|
||||
try {
|
||||
wait(5000);
|
||||
if (!finished) {
|
||||
Log.w(TAG, "Timeout waiting for recording to finish");
|
||||
break;
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
Log.w(TAG, "Interrupted while waiting for recording to finish", ie);
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start(final OutputStream outputStream) {
|
||||
public void start() {
|
||||
new Thread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
byte[] audioRecordData = new byte[bufferSize];
|
||||
ByteBuffer[] codecInputBuffers = mediaCodec.getInputBuffers();
|
||||
ByteBuffer[] codecOutputBuffers = mediaCodec.getOutputBuffers();
|
||||
() -> {
|
||||
this.startTime = System.nanoTime();
|
||||
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
byte[] audioRecordData = new byte[bufferSize];
|
||||
MediaMuxer muxer = null;
|
||||
int audioTrackIndex = -1;
|
||||
boolean muxerStarted = false;
|
||||
|
||||
try {
|
||||
muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
||||
|
||||
while (true) {
|
||||
boolean running = isRunning();
|
||||
|
||||
handleCodecInput(audioRecord, audioRecordData, mediaCodec, running);
|
||||
int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 2000);
|
||||
|
||||
while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
if (codecOutputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
// Get the format to add track to muxer
|
||||
if (muxerStarted) {
|
||||
Log.w(TAG, "Output format changed after muxer started, ignoring");
|
||||
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
continue;
|
||||
}
|
||||
MediaFormat newFormat = mediaCodec.getOutputFormat();
|
||||
Log.d(TAG, "Output format changed: " + newFormat);
|
||||
audioTrackIndex = muxer.addTrack(newFormat);
|
||||
muxer.start();
|
||||
muxerStarted = true;
|
||||
Log.d(TAG, "Muxer started");
|
||||
} else if (codecOutputBufferIndex >= 0) {
|
||||
ByteBuffer encodedData = mediaCodec.getOutputBuffer(codecOutputBufferIndex);
|
||||
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||
// Codec config info, not actual data, skip it
|
||||
Log.d(TAG, "Ignoring CODEC_CONFIG buffer");
|
||||
bufferInfo.size = 0;
|
||||
}
|
||||
|
||||
if (bufferInfo.size != 0 && encodedData != null) {
|
||||
if (!muxerStarted) {
|
||||
Log.w(TAG, "Muxer not started, dropping encoded data");
|
||||
} else {
|
||||
// Adjust ByteBuffer to match BufferInfo
|
||||
encodedData.position(bufferInfo.offset);
|
||||
encodedData.limit(bufferInfo.offset + bufferInfo.size);
|
||||
|
||||
// Write sample data to muxer
|
||||
muxer.writeSampleData(audioTrackIndex, encodedData, bufferInfo);
|
||||
}
|
||||
}
|
||||
|
||||
mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false);
|
||||
|
||||
// Check for end of stream
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
Log.d(TAG, "End of stream reached");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
}
|
||||
|
||||
if (!running && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error during encoding", e);
|
||||
} finally {
|
||||
try {
|
||||
if (muxerStarted && muxer != null) {
|
||||
try {
|
||||
muxer.stop();
|
||||
Log.d(TAG, "Muxer stopped");
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(TAG, "Muxer already stopped", e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error stopping muxer", e);
|
||||
}
|
||||
|
||||
if (muxer != null) {
|
||||
try {
|
||||
muxer.release();
|
||||
Log.d(TAG, "Muxer released");
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error releasing muxer", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
boolean running = isRunning();
|
||||
|
||||
handleCodecInput(
|
||||
audioRecord, audioRecordData, mediaCodec, codecInputBuffers, running);
|
||||
handleCodecOutput(mediaCodec, codecOutputBuffers, bufferInfo, outputStream);
|
||||
|
||||
if (!running) break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
} finally {
|
||||
mediaCodec.stop();
|
||||
audioRecord.stop();
|
||||
|
||||
mediaCodec.release();
|
||||
audioRecord.release();
|
||||
|
||||
Util.close(outputStream);
|
||||
setFinished();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error stopping codec", e);
|
||||
}
|
||||
|
||||
try {
|
||||
audioRecord.stop();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error stopping audio record", e);
|
||||
}
|
||||
|
||||
try {
|
||||
mediaCodec.release();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error releasing codec", e);
|
||||
}
|
||||
|
||||
try {
|
||||
audioRecord.release();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error releasing audio record", e);
|
||||
}
|
||||
|
||||
setFinished();
|
||||
}
|
||||
},
|
||||
AudioCodec.class.getSimpleName())
|
||||
@@ -108,75 +210,35 @@ public class AudioCodec {
|
||||
}
|
||||
|
||||
private void handleCodecInput(
|
||||
AudioRecord audioRecord,
|
||||
byte[] audioRecordData,
|
||||
MediaCodec mediaCodec,
|
||||
ByteBuffer[] codecInputBuffers,
|
||||
boolean running) {
|
||||
AudioRecord audioRecord, byte[] audioRecordData, MediaCodec mediaCodec, boolean running) {
|
||||
int length = audioRecord.read(audioRecordData, 0, audioRecordData.length);
|
||||
|
||||
if (length < 0) {
|
||||
Log.w(TAG, "Error reading from AudioRecord: " + length);
|
||||
return;
|
||||
}
|
||||
|
||||
int codecInputBufferIndex = mediaCodec.dequeueInputBuffer(10 * 1000);
|
||||
|
||||
if (codecInputBufferIndex >= 0) {
|
||||
ByteBuffer codecBuffer = codecInputBuffers[codecInputBufferIndex];
|
||||
codecBuffer.clear();
|
||||
codecBuffer.put(audioRecordData);
|
||||
mediaCodec.queueInputBuffer(
|
||||
codecInputBufferIndex, 0, length, 0, running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
}
|
||||
}
|
||||
ByteBuffer codecBuffer = mediaCodec.getInputBuffer(codecInputBufferIndex);
|
||||
|
||||
private void handleCodecOutput(
|
||||
MediaCodec mediaCodec,
|
||||
ByteBuffer[] codecOutputBuffers,
|
||||
MediaCodec.BufferInfo bufferInfo,
|
||||
OutputStream outputStream)
|
||||
throws IOException {
|
||||
int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
|
||||
while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
if (codecOutputBufferIndex >= 0) {
|
||||
ByteBuffer encoderOutputBuffer = codecOutputBuffers[codecOutputBufferIndex];
|
||||
|
||||
encoderOutputBuffer.position(bufferInfo.offset);
|
||||
encoderOutputBuffer.limit(bufferInfo.offset + bufferInfo.size);
|
||||
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG)
|
||||
!= MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
|
||||
byte[] header = createAdtsHeader(bufferInfo.size - bufferInfo.offset);
|
||||
|
||||
outputStream.write(header);
|
||||
|
||||
byte[] data = new byte[encoderOutputBuffer.remaining()];
|
||||
encoderOutputBuffer.get(data);
|
||||
outputStream.write(data);
|
||||
}
|
||||
|
||||
encoderOutputBuffer.clear();
|
||||
|
||||
mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false);
|
||||
} else if (codecOutputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||
codecOutputBuffers = mediaCodec.getOutputBuffers();
|
||||
if (codecBuffer != null) {
|
||||
codecBuffer.clear();
|
||||
codecBuffer.put(audioRecordData, 0, length);
|
||||
long presentationTimeUs = getPresentationTimeUs();
|
||||
mediaCodec.queueInputBuffer(
|
||||
codecInputBufferIndex,
|
||||
0,
|
||||
length,
|
||||
presentationTimeUs,
|
||||
running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
}
|
||||
|
||||
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] createAdtsHeader(int length) {
|
||||
int frameLength = length + 7;
|
||||
byte[] adtsHeader = new byte[7];
|
||||
|
||||
adtsHeader[0] = (byte) 0xFF; // Sync Word
|
||||
adtsHeader[1] = (byte) 0xF1; // MPEG-4, Layer (0), No CRC
|
||||
adtsHeader[2] = (byte) ((MediaCodecInfo.CodecProfileLevel.AACObjectLC - 1) << 6);
|
||||
adtsHeader[2] |= (((byte) getSampleRateIndex()) << 2);
|
||||
adtsHeader[2] |= (((byte) CHANNELS) >> 2);
|
||||
adtsHeader[3] = (byte) (((CHANNELS & 3) << 6) | ((frameLength >> 11) & 0x03));
|
||||
adtsHeader[4] = (byte) ((frameLength >> 3) & 0xFF);
|
||||
adtsHeader[5] = (byte) (((frameLength & 0x07) << 5) | 0x1f);
|
||||
adtsHeader[6] = (byte) 0xFC;
|
||||
|
||||
return adtsHeader;
|
||||
private long getPresentationTimeUs() {
|
||||
return (System.nanoTime() - startTime) / 1000;
|
||||
}
|
||||
|
||||
private AudioRecord createAudioRecord(int bufferSize) {
|
||||
@@ -190,11 +252,9 @@ public class AudioCodec {
|
||||
|
||||
private MediaCodec createMediaCodec(int bufferSize) throws IOException {
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
|
||||
MediaFormat mediaFormat = new MediaFormat();
|
||||
|
||||
mediaFormat.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
|
||||
mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, getSampleRate());
|
||||
mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createAudioFormat("audio/mp4a-latm", getSampleRate(), CHANNELS);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, getBitRate());
|
||||
mediaFormat.setInteger(
|
||||
@@ -215,12 +275,6 @@ public class AudioCodec {
|
||||
return Prefs.isHardCompressionEnabled(context) ? SAMPLE_RATE_WORSE : SAMPLE_RATE_BALANCED;
|
||||
}
|
||||
|
||||
private int getSampleRateIndex() {
|
||||
return Prefs.isHardCompressionEnabled(context)
|
||||
? SAMPLE_RATE_INDEX_WORSE
|
||||
: SAMPLE_RATE_INDEX_BALANCED;
|
||||
}
|
||||
|
||||
private int getBitRate() {
|
||||
return Prefs.isHardCompressionEnabled(context) ? BIT_RATE_WORSE : BIT_RATE_BALANCED;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.NonNull;
|
||||
import chat.delta.util.ListenableFuture;
|
||||
import chat.delta.util.SettableFuture;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ThreadUtil;
|
||||
@@ -24,8 +25,17 @@ public class AudioRecorder {
|
||||
private final Context context;
|
||||
private final PersistentBlobProvider blobProvider;
|
||||
|
||||
private AudioCodec audioCodec;
|
||||
private Uri captureUri;
|
||||
private final AtomicReference<RecordingState> recordingState = new AtomicReference<>(null);
|
||||
|
||||
private static class RecordingState {
|
||||
final AudioCodec audioCodec;
|
||||
final String outputFilePath;
|
||||
|
||||
RecordingState(AudioCodec audioCodec, String outputFilePath) {
|
||||
this.audioCodec = audioCodec;
|
||||
this.outputFilePath = outputFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
public AudioRecorder(@NonNull Context context) {
|
||||
this.context = context;
|
||||
@@ -39,24 +49,22 @@ public class AudioRecorder {
|
||||
() -> {
|
||||
Log.w(TAG, "Running startRecording() + " + Thread.currentThread().getId());
|
||||
try {
|
||||
if (audioCodec != null) {
|
||||
RecordingState currentState = recordingState.get();
|
||||
if (currentState != null) {
|
||||
throw new AssertionError("We can only record once at a time.");
|
||||
}
|
||||
|
||||
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
|
||||
// Create temporary file for M4A output
|
||||
File outputFile = File.createTempFile("voice", ".m4a", context.getCacheDir());
|
||||
String outputFilePath = outputFile.getAbsolutePath();
|
||||
|
||||
captureUri =
|
||||
blobProvider.create(
|
||||
context,
|
||||
new ParcelFileDescriptor.AutoCloseInputStream(fds[0]),
|
||||
MediaUtil.AUDIO_AAC,
|
||||
"voice.aac",
|
||||
null);
|
||||
audioCodec = new AudioCodec(context);
|
||||
AudioCodec audioCodec = new AudioCodec(context, outputFilePath);
|
||||
recordingState.set(new RecordingState(audioCodec, outputFilePath));
|
||||
|
||||
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
|
||||
audioCodec.start();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
recordingState.set(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -68,24 +76,39 @@ public class AudioRecorder {
|
||||
|
||||
executor.execute(
|
||||
() -> {
|
||||
if (audioCodec == null) {
|
||||
RecordingState state = recordingState.getAndSet(null);
|
||||
|
||||
if (state == null || state.audioCodec == null) {
|
||||
sendToFuture(
|
||||
future, new IOException("MediaRecorder was never initialized successfully!"));
|
||||
return;
|
||||
}
|
||||
|
||||
audioCodec.stop();
|
||||
state.audioCodec.stop();
|
||||
|
||||
try {
|
||||
long size = MediaUtil.getMediaSize(context, captureUri);
|
||||
File outputFile = new File(state.outputFilePath);
|
||||
|
||||
if (!outputFile.exists()) {
|
||||
sendToFuture(future, new IOException("Output file does not exist"));
|
||||
return;
|
||||
}
|
||||
|
||||
long size = outputFile.length();
|
||||
|
||||
// Create blob using synchronous file-based method
|
||||
Uri captureUri =
|
||||
blobProvider.create(context, outputFile, MediaUtil.AUDIO_M4A, "voice.m4a", size);
|
||||
|
||||
if (!outputFile.delete()) {
|
||||
Log.d(TAG, "Temp file already moved or couldn't be deleted");
|
||||
}
|
||||
|
||||
sendToFuture(future, new Pair<>(captureUri, size));
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
Log.w(TAG, "Failed to create blob from recording", ioe);
|
||||
sendToFuture(future, ioe);
|
||||
}
|
||||
|
||||
audioCodec = null;
|
||||
captureUri = null;
|
||||
});
|
||||
|
||||
return future;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.thoughtcrime.securesms.calls;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.telecom.CallEndpointCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import java.util.List;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
/** Bottom sheet dialog for selecting audio output device */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
public class AudioDevicePickerDialog extends BottomSheetDialog {
|
||||
private static final String TAG = AudioDevicePickerDialog.class.getSimpleName();
|
||||
|
||||
public interface OnDeviceSelectedListener {
|
||||
void onDeviceSelected(CallEndpointCompat endpoint);
|
||||
}
|
||||
|
||||
public AudioDevicePickerDialog(
|
||||
@NonNull Context context,
|
||||
@NonNull List<CallEndpointCompat> endpoints,
|
||||
CallEndpointCompat currentEndpoint,
|
||||
@NonNull OnDeviceSelectedListener listener) {
|
||||
super(context);
|
||||
|
||||
setContentView(R.layout.dialog_audio_device_picker);
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.audio_device_list);
|
||||
if (recyclerView == null) {
|
||||
Log.e(TAG, "RecyclerView not found in layout");
|
||||
return;
|
||||
}
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
AudioDeviceAdapter adapter =
|
||||
new AudioDeviceAdapter(
|
||||
endpoints,
|
||||
currentEndpoint,
|
||||
endpoint -> {
|
||||
listener.onDeviceSelected(endpoint);
|
||||
dismiss();
|
||||
});
|
||||
|
||||
recyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
/** RecyclerView adapter for audio devices */
|
||||
private static class AudioDeviceAdapter
|
||||
extends RecyclerView.Adapter<AudioDeviceAdapter.ViewHolder> {
|
||||
|
||||
private final List<CallEndpointCompat> endpoints;
|
||||
private final CallEndpointCompat currentEndpoint;
|
||||
private final OnDeviceSelectedListener listener;
|
||||
|
||||
AudioDeviceAdapter(
|
||||
List<CallEndpointCompat> endpoints,
|
||||
CallEndpointCompat currentEndpoint,
|
||||
OnDeviceSelectedListener listener) {
|
||||
this.endpoints = endpoints;
|
||||
this.currentEndpoint = currentEndpoint;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view =
|
||||
LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_audio_device, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
CallEndpointCompat endpoint = endpoints.get(position);
|
||||
boolean isCurrent =
|
||||
currentEndpoint != null
|
||||
&& endpoint.getIdentifier().equals(currentEndpoint.getIdentifier());
|
||||
|
||||
holder.bind(endpoint, isCurrent, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return endpoints.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final ImageView iconView;
|
||||
private final TextView nameView;
|
||||
private final ImageView checkView;
|
||||
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
iconView = itemView.findViewById(R.id.device_icon);
|
||||
nameView = itemView.findViewById(R.id.device_name);
|
||||
checkView = itemView.findViewById(R.id.device_check);
|
||||
}
|
||||
|
||||
void bind(CallEndpointCompat endpoint, boolean isCurrent, OnDeviceSelectedListener listener) {
|
||||
nameView.setText(endpoint.getName());
|
||||
|
||||
int iconRes = CallUtil.getIconResByCallEndpoint(endpoint);
|
||||
iconView.setImageResource(iconRes);
|
||||
|
||||
checkView.setVisibility(isCurrent ? View.VISIBLE : View.GONE);
|
||||
|
||||
itemView.setOnClickListener(v -> listener.onDeviceSelected(endpoint));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,538 @@
|
||||
package org.thoughtcrime.securesms.calls;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.media.ToneGenerator;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import org.thoughtcrime.securesms.webrtc.WebRTCClient;
|
||||
import org.webrtc.AudioTrack;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
/**
|
||||
* Foreground service for VoIP calls
|
||||
*
|
||||
* <p>Required to post CallStyle notifications on Android 12+. Owns WebRTC resources and keeps call
|
||||
* alive.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public class CallService extends Service implements WebRTCClient.Callbacks {
|
||||
|
||||
private static final String TAG = CallService.class.getSimpleName();
|
||||
|
||||
private final IBinder binder = new LocalBinder();
|
||||
|
||||
// WebRTC Resources
|
||||
private WebRTCClient webRTCClient;
|
||||
private MediaStreamManager mediaStreamManager;
|
||||
|
||||
// Ringtone Resources
|
||||
private Ringtone ringtone;
|
||||
private AudioManager audioManager;
|
||||
private AudioFocusRequest audioFocusRequest;
|
||||
private ToneGenerator toneGenerator;
|
||||
private Runnable toneLoopRunnable;
|
||||
|
||||
private CallCoordinator callCoordinator;
|
||||
|
||||
public class LocalBinder extends Binder {
|
||||
CallService getService() {
|
||||
return CallService.this;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.d(TAG, "CallService onCreate");
|
||||
|
||||
callCoordinator = CallCoordinator.getInstance(getApplicationContext());
|
||||
|
||||
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
Log.d(TAG, "CallService created");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.d(TAG, "CallService started");
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
// Initialization
|
||||
|
||||
/** Initialize call infrastructure Does NOT start camera/microphone */
|
||||
public void initializeCall() {
|
||||
Log.d(TAG, "initializeCall (Infrastructure only)");
|
||||
|
||||
if (webRTCClient != null) {
|
||||
Log.w(TAG, "Infrastructure already initialized, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
webRTCClient = new WebRTCClient(getApplicationContext(), this);
|
||||
|
||||
mediaStreamManager =
|
||||
new MediaStreamManager(getApplicationContext(), webRTCClient.getPeerConnectionFactory());
|
||||
|
||||
fetchIceServersAndSetup();
|
||||
}
|
||||
|
||||
private void fetchIceServersAndSetup() {
|
||||
new Thread(
|
||||
() -> {
|
||||
if (!callCoordinator.hasActiveCall()) {
|
||||
Log.d(TAG, "Call ended before ICE fetch, aborting");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String iceServersJson = callCoordinator.fetchIceServers();
|
||||
Log.d(TAG, "ICE servers fetched: " + iceServersJson);
|
||||
|
||||
webRTCClient.configure(iceServersJson);
|
||||
|
||||
Log.d(TAG, "Infrastructure initialized, waiting for media capture");
|
||||
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Failed to fetch ICE servers", e);
|
||||
callCoordinator.reportError("Failed to connect: " + e.getMessage());
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start camera/microphone 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)");
|
||||
|
||||
if (webRTCClient != null && webRTCClient.hasLocalMediaStream()) {
|
||||
Log.w(TAG, "Media already initialized, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaStreamManager == null) {
|
||||
Log.e(TAG, "MediaStreamManager not initialized");
|
||||
callCoordinator.reportError("MediaStreamManager not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!callCoordinator.hasMicrophonePermission()) {
|
||||
Log.e(TAG, "Microphone permission not granted, cannot start call");
|
||||
callCoordinator.reportError("Microphone permission is required for calls");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean startsWithVideo = callCoordinator.isStartsWithVideo();
|
||||
|
||||
Log.d(TAG, "Creating media stream with video: " + startsWithVideo);
|
||||
|
||||
mediaStreamManager.createMediaStream(
|
||||
new MediaStreamManager.Callback() {
|
||||
@Override
|
||||
public void onMediaStreamReady(MediaStream stream) {
|
||||
Log.d(TAG, "Media stream ready");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ringtone Management
|
||||
|
||||
public void startIncomingRingtone() {
|
||||
Log.d(TAG, "startIncomingRingtone");
|
||||
|
||||
if (ringtone != null && ringtone.isPlaying()) {
|
||||
Log.d(TAG, "Ringtone already playing");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get system default ringtone URI
|
||||
Uri ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
|
||||
|
||||
if (ringtoneUri == null) {
|
||||
Log.w(TAG, "No default ringtone available");
|
||||
return;
|
||||
}
|
||||
|
||||
ringtone = RingtoneManager.getRingtone(getApplicationContext(), ringtoneUri);
|
||||
|
||||
if (ringtone == null) {
|
||||
Log.e(TAG, "Failed to create Ringtone from URI: " + ringtoneUri);
|
||||
return;
|
||||
}
|
||||
|
||||
AudioAttributes audioAttributes =
|
||||
new AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.setLegacyStreamType(AudioManager.STREAM_RING)
|
||||
.build();
|
||||
|
||||
ringtone.setAudioAttributes(audioAttributes);
|
||||
|
||||
// Request audio focus
|
||||
audioFocusRequest =
|
||||
new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.setWillPauseWhenDucked(false)
|
||||
.build();
|
||||
|
||||
int result = audioManager.requestAudioFocus(audioFocusRequest);
|
||||
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Log.w(TAG, "Audio focus not granted");
|
||||
}
|
||||
|
||||
ringtone.play();
|
||||
Log.d(TAG, "Ringtone started playing");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to start ringtone", e);
|
||||
// Clean up on error
|
||||
stopRingtone();
|
||||
}
|
||||
}
|
||||
|
||||
public void startOutgoingRingtone() {
|
||||
Log.d(TAG, "startOutgoingRingtone");
|
||||
|
||||
if (toneGenerator != null) {
|
||||
Log.d(TAG, "Tone already playing");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AudioAttributes audioAttributes =
|
||||
new AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
|
||||
.build();
|
||||
|
||||
audioFocusRequest =
|
||||
new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.setWillPauseWhenDucked(false)
|
||||
.build();
|
||||
|
||||
int result = audioManager.requestAudioFocus(audioFocusRequest);
|
||||
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Log.w(TAG, "Audio focus not granted");
|
||||
}
|
||||
|
||||
// Create ToneGenerator
|
||||
toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80);
|
||||
|
||||
boolean started = toneGenerator.startTone(ToneGenerator.TONE_SUP_RINGTONE, -1);
|
||||
|
||||
if (started) {
|
||||
Log.d(TAG, "Outgoing ringback tone started playing");
|
||||
} else {
|
||||
Log.e(TAG, "Failed to start tone");
|
||||
stopRingtone();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to start outgoing ringtone", e);
|
||||
stopRingtone();
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop playing ringtone */
|
||||
public void stopRingtone() {
|
||||
Log.d(TAG, "stopRingtone");
|
||||
|
||||
if (toneGenerator != null) {
|
||||
try {
|
||||
toneGenerator.stopTone();
|
||||
toneGenerator.release();
|
||||
Log.d(TAG, "ToneGenerator stopped and released");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error stopping ToneGenerator", e);
|
||||
}
|
||||
toneGenerator = null;
|
||||
}
|
||||
|
||||
if (ringtone != null) {
|
||||
try {
|
||||
if (ringtone.isPlaying()) {
|
||||
ringtone.stop();
|
||||
Log.d(TAG, "Ringtone stopped");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error stopping ringtone", e);
|
||||
}
|
||||
ringtone = null;
|
||||
}
|
||||
|
||||
if (audioFocusRequest != null && audioManager != null) {
|
||||
audioManager.abandonAudioFocusRequest(audioFocusRequest);
|
||||
audioFocusRequest = null;
|
||||
Log.d(TAG, "Audio focus abandoned");
|
||||
}
|
||||
}
|
||||
|
||||
// Call Control
|
||||
|
||||
public void startOutgoingCall() {
|
||||
Log.d(TAG, "startOutgoingCall");
|
||||
|
||||
if (webRTCClient == null) {
|
||||
Log.e(TAG, "Cannot start call, not initialized");
|
||||
callCoordinator.reportError("Service not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webRTCClient.hasLocalMediaStream()) {
|
||||
Log.e(TAG, "Cannot start call, media not ready");
|
||||
callCoordinator.reportError("Media not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
webRTCClient.startOutgoingCall();
|
||||
}
|
||||
|
||||
public void answerIncomingCall() {
|
||||
Log.d(TAG, "answerIncomingCall");
|
||||
|
||||
String offerSdp = callCoordinator.getPendingOfferSdp();
|
||||
|
||||
if (offerSdp == null) {
|
||||
Log.e(TAG, "No offer SDP available");
|
||||
callCoordinator.reportError("Call data missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (webRTCClient == null) {
|
||||
Log.e(TAG, "Cannot answer call, not initialized");
|
||||
callCoordinator.reportError("Service not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webRTCClient.hasLocalMediaStream()) {
|
||||
Log.e(TAG, "Cannot answer, media not ready");
|
||||
callCoordinator.reportError("Media not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
callCoordinator.clearPendingOfferSdp();
|
||||
|
||||
webRTCClient.acceptIncomingCall(offerSdp);
|
||||
}
|
||||
|
||||
public void handleAnswerSdp(String answerSdp) {
|
||||
Log.d(TAG, "handleAnswerSdp");
|
||||
|
||||
if (webRTCClient == null) {
|
||||
Log.e(TAG, "Cannot handle answer, not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
webRTCClient.handleAnswerSdp(answerSdp);
|
||||
}
|
||||
|
||||
public void setAudioEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setAudioEnabled: " + enabled);
|
||||
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.setAudioEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVideoEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setVideoEnabled: " + enabled);
|
||||
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.setVideoEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMutedState(boolean audioEnabled, boolean videoEnabled) {
|
||||
Log.d(TAG, "sendMutedState: audio=" + audioEnabled + ", video=" + videoEnabled);
|
||||
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.sendMutedState(audioEnabled, videoEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void switchCamera() {
|
||||
Log.d(TAG, "switchCamera");
|
||||
|
||||
if (mediaStreamManager != null) {
|
||||
mediaStreamManager.switchCamera(
|
||||
new MediaStreamManager.CameraSwitchCallback() {
|
||||
@Override
|
||||
public void onCameraSwitch(boolean isFrontCamera) {
|
||||
callCoordinator.updateFrontCamera(isFrontCamera);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "Camera switch failed: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void endCall() {
|
||||
Log.d(TAG, "endCall");
|
||||
|
||||
disposeWebRTC();
|
||||
|
||||
stopService();
|
||||
}
|
||||
|
||||
// WebRTCClient.Callbacks
|
||||
|
||||
@Override
|
||||
public void onOfferReady(String offerSdp) {
|
||||
Log.d(TAG, "onOfferReady callback");
|
||||
|
||||
callCoordinator.handleOfferReady(offerSdp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnswerReady(String answerSdp) {
|
||||
Log.d(TAG, "onAnswerReady callback");
|
||||
|
||||
callCoordinator.handleAnswerReady(answerSdp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteVideoTrack(VideoTrack videoTrack) {
|
||||
Log.d(TAG, "onRemoteVideoTrack callback");
|
||||
|
||||
callCoordinator.updateRemoteVideoTrack(videoTrack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteAudioTrack(AudioTrack audioTrack) {
|
||||
Log.d(TAG, "onRemoteAudioTrack callback");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChanged(PeerConnection.PeerConnectionState state) {
|
||||
Log.d(TAG, "onConnectionStateChanged: " + state);
|
||||
|
||||
callCoordinator.updateConnectionState(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteMutedStateChanged(boolean audioEnabled, boolean videoEnabled) {
|
||||
Log.d(TAG, "onRemoteMutedStateChanged: audio=" + audioEnabled + ", video=" + videoEnabled);
|
||||
|
||||
callCoordinator.updateRemoteMutedState(audioEnabled, videoEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelayUsageChanged(Boolean isRelayUsed) {
|
||||
Log.d(TAG, "onRelayUsageChanged: " + isRelayUsed);
|
||||
|
||||
callCoordinator.updateRelayUsage(isRelayUsed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "onError: " + error);
|
||||
|
||||
callCoordinator.reportError(error);
|
||||
}
|
||||
|
||||
// Foreground Notification
|
||||
|
||||
public void startForegroundWithNotification(int id, Notification notification) {
|
||||
Log.d(TAG, "Starting foreground with notification id: " + id);
|
||||
startForeground(id, notification);
|
||||
}
|
||||
|
||||
public void stopForegroundAndDismiss() {
|
||||
Log.d(TAG, "Stopping foreground and dismissing notification");
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
|
||||
private void disposeWebRTC() {
|
||||
Log.d(TAG, "Disposing WebRTC resources");
|
||||
|
||||
if (mediaStreamManager != null) {
|
||||
mediaStreamManager.dispose();
|
||||
mediaStreamManager = null;
|
||||
}
|
||||
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.dispose();
|
||||
webRTCClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void stopService() {
|
||||
Log.d(TAG, "Stopping CallService");
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.d(TAG, "CallService onDestroy");
|
||||
|
||||
stopRingtone();
|
||||
|
||||
disposeWebRTC();
|
||||
|
||||
Log.d(TAG, "CallService destroyed");
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,141 @@
|
||||
package org.thoughtcrime.securesms.calls;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Base64;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.telecom.CallEndpointCompat;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
|
||||
public class CallUtil {
|
||||
private static final String TAG = CallUtil.class.getSimpleName();
|
||||
|
||||
public static void startCall(Activity activity, int chatId, boolean hasVideo) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_camera_denied))
|
||||
.onAllGranted(() -> {
|
||||
int accId = DcHelper.getContext(activity).getAccountId();
|
||||
startCall(activity, accId, chatId, hasVideo);
|
||||
})
|
||||
.execute();
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void startAudioCall(Context context, int chatId) {
|
||||
Log.d(TAG, "Starting audio call to " + chatId);
|
||||
startCall(context, chatId, false);
|
||||
}
|
||||
|
||||
public static void startCall(Context context, int accId, int chatId, boolean hasVideo) {
|
||||
Intent intent = new Intent(context, CallActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
|
||||
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
|
||||
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
|
||||
intent.putExtra(CallActivity.EXTRA_HASH, "#startCall");
|
||||
context.startActivity(intent);
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void startVideoCall(Context context, int chatId) {
|
||||
Log.d(TAG, "Starting video call to " + chatId);
|
||||
startCall(context, chatId, true);
|
||||
}
|
||||
|
||||
public static void openCall(Context context, int accId, int chatId, int callId, String payload, boolean hasVideo) {
|
||||
String base64 = Base64.encodeToString(payload.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
|
||||
String hash = "";
|
||||
try {
|
||||
hash = "#offerIncomingCall=" + URLEncoder.encode(base64, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.e(TAG, "Error", e);
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private static void startCall(Context context, int chatId, boolean startsWithVideo) {
|
||||
if (chatId < 0) {
|
||||
Log.e(TAG, "Cannot start call: wrong chatId");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(context, CallActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
|
||||
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
|
||||
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
|
||||
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
|
||||
intent.putExtra(CallActivity.EXTRA_HASH, hash);
|
||||
context.startActivity(intent);
|
||||
CallCoordinator coordinator = CallCoordinator.getInstance(context);
|
||||
|
||||
if (coordinator.hasActiveCall()) {
|
||||
Toast.makeText(context, R.string.already_in_call, Toast.LENGTH_SHORT).show();
|
||||
Intent intent = new Intent(context, CallActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
return;
|
||||
}
|
||||
|
||||
int accId = DcHelper.getContext(context).getAccountId();
|
||||
coordinator.initiateOutgoingCall(accId, chatId, startsWithVideo);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
protected static Icon getIconFromChat(Context context, DcChat dcChat) {
|
||||
Log.d(TAG, "getIconFromChat: thread=" + Thread.currentThread().getName());
|
||||
|
||||
try {
|
||||
Recipient recipient = new Recipient(context, dcChat);
|
||||
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
|
||||
|
||||
int wh = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
|
||||
Bitmap bitmap;
|
||||
|
||||
if (contactPhoto != null) {
|
||||
bitmap =
|
||||
GlideApp.with(context)
|
||||
.asBitmap()
|
||||
.load(contactPhoto)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.circleCrop()
|
||||
.submit(wh, wh)
|
||||
.get();
|
||||
} else {
|
||||
Drawable drawable =
|
||||
recipient
|
||||
.getFallbackContactPhoto()
|
||||
.asDrawable(context, recipient.getFallbackAvatarColor());
|
||||
bitmap = BitmapUtil.createFromDrawable(drawable, wh, wh);
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Bitmap loaded: "
|
||||
+ bitmap.getWidth()
|
||||
+ "x"
|
||||
+ bitmap.getHeight()
|
||||
+ ", config="
|
||||
+ bitmap.getConfig()
|
||||
+ ", recycled="
|
||||
+ bitmap.isRecycled());
|
||||
Icon icon = Icon.createWithBitmap(bitmap);
|
||||
Log.d(TAG, "Icon created successfully");
|
||||
return icon;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to load caller icon", e);
|
||||
}
|
||||
|
||||
Log.w(TAG, "Returning null icon");
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static String getNameFromChat(DcChat dcChat) {
|
||||
return dcChat.getName();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static int getIconResByCallEndpoint(CallEndpointCompat endpoint) {
|
||||
int iconRes;
|
||||
switch (endpoint.getType()) {
|
||||
case CallEndpointCompat.TYPE_EARPIECE:
|
||||
iconRes = R.drawable.ic_phone_in_talk;
|
||||
break;
|
||||
case CallEndpointCompat.TYPE_SPEAKER:
|
||||
iconRes = R.drawable.ic_volume_up;
|
||||
break;
|
||||
case CallEndpointCompat.TYPE_BLUETOOTH:
|
||||
iconRes = R.drawable.ic_bluetooth_audio;
|
||||
break;
|
||||
case CallEndpointCompat.TYPE_WIRED_HEADSET:
|
||||
iconRes = R.drawable.ic_headset;
|
||||
break;
|
||||
case CallEndpointCompat.TYPE_STREAMING:
|
||||
iconRes = R.drawable.ic_cast;
|
||||
break;
|
||||
default:
|
||||
iconRes = R.drawable.ic_volume_up;
|
||||
break;
|
||||
}
|
||||
return iconRes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
package org.thoughtcrime.securesms.calls;
|
||||
|
||||
import android.app.Application;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.telecom.DisconnectCause;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.telecom.CallEndpointCompat;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public class CallViewModel extends AndroidViewModel {
|
||||
|
||||
private static final String TAG = CallViewModel.class.getSimpleName();
|
||||
|
||||
private final CallCoordinator callCoordinator;
|
||||
|
||||
// UI State LiveData (from Coordinator)
|
||||
|
||||
private final LiveData<VideoTrack> localVideoTrack;
|
||||
private final LiveData<VideoTrack> remoteVideoTrack;
|
||||
private final LiveData<Boolean> localAudioEnabled;
|
||||
private final LiveData<Boolean> localVideoEnabled;
|
||||
private final LiveData<Boolean> remoteAudioEnabled;
|
||||
private final LiveData<Boolean> remoteVideoEnabled;
|
||||
private final LiveData<Boolean> isRelayUsed;
|
||||
private final LiveData<String> errorMessage;
|
||||
private final LiveData<String> displayName;
|
||||
private final LiveData<Icon> displayIcon;
|
||||
private final LiveData<Boolean> outgoingCallPlaced;
|
||||
private final LiveData<Boolean> answeredElsewhere;
|
||||
private final LiveData<CallEndpointCompat> currentAudioEndpoint;
|
||||
private final LiveData<List<CallEndpointCompat>> availableAudioEndpoints;
|
||||
private final LiveData<Boolean> isFrontCamera;
|
||||
|
||||
// Translated from coordinator's connectionState
|
||||
private final MediatorLiveData<CallState> callState;
|
||||
|
||||
// Observer References for one-time observe
|
||||
private Observer<VideoTrack> answerCallObserver;
|
||||
private Observer<VideoTrack> startOutgoingCallObserver;
|
||||
|
||||
private final AtomicBoolean hasCallEnded = new AtomicBoolean(false);
|
||||
|
||||
// User-facing call states
|
||||
public enum CallState {
|
||||
INITIALIZING,
|
||||
PROMPTING_USER_ACCEPT,
|
||||
RINGING,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
RECONNECTING,
|
||||
ANSWERED_ELSEWHERE,
|
||||
ENDED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
public CallViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
|
||||
this.callCoordinator = CallCoordinator.getInstance(application);
|
||||
|
||||
// Setup LiveData observations
|
||||
this.localVideoTrack = callCoordinator.getLocalVideoTrack();
|
||||
this.remoteVideoTrack = callCoordinator.getRemoteVideoTrack();
|
||||
this.localAudioEnabled = callCoordinator.getLocalAudioEnabled();
|
||||
this.localVideoEnabled = callCoordinator.getLocalVideoEnabled();
|
||||
this.remoteAudioEnabled = callCoordinator.getRemoteAudioEnabled();
|
||||
this.remoteVideoEnabled = callCoordinator.getRemoteVideoEnabled();
|
||||
this.isRelayUsed = callCoordinator.getIsRelayUsed();
|
||||
this.errorMessage = callCoordinator.getErrorMessage();
|
||||
this.displayName = callCoordinator.getDisplayName();
|
||||
this.displayIcon = callCoordinator.getDisplayIcon();
|
||||
this.outgoingCallPlaced = callCoordinator.getOutgoingCallPlaced();
|
||||
this.answeredElsewhere = callCoordinator.getAnsweredElsewhere();
|
||||
this.currentAudioEndpoint = callCoordinator.getCurrentAudioEndpoint();
|
||||
this.availableAudioEndpoints = callCoordinator.getAvailableAudioEndpoints();
|
||||
this.isFrontCamera = callCoordinator.getIsFrontCamera();
|
||||
|
||||
this.callState = new MediatorLiveData<>(CallState.INITIALIZING);
|
||||
|
||||
setupConnectionStateObserver();
|
||||
|
||||
Log.d(TAG, "CallViewModel created");
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
Log.d(TAG, "Initializing CallViewModel");
|
||||
|
||||
callCoordinator.setActiveCallViewModel(this);
|
||||
|
||||
if (callCoordinator.isIncomingCall()) {
|
||||
callState.setValue(CallState.PROMPTING_USER_ACCEPT);
|
||||
} else {
|
||||
callState.setValue(CallState.CONNECTING);
|
||||
}
|
||||
|
||||
Log.d(TAG, "CallViewModel initialized");
|
||||
}
|
||||
|
||||
private void setupConnectionStateObserver() {
|
||||
callState.addSource(
|
||||
callCoordinator.getConnectionState(),
|
||||
state -> {
|
||||
if (callState.getValue() == CallState.ANSWERED_ELSEWHERE) {
|
||||
return;
|
||||
}
|
||||
|
||||
CallState newState = translateConnectionState(state);
|
||||
|
||||
if (callState.getValue() != newState) {
|
||||
callState.setValue(newState);
|
||||
}
|
||||
|
||||
if (state == PeerConnection.PeerConnectionState.FAILED
|
||||
|| state == PeerConnection.PeerConnectionState.CLOSED) {
|
||||
hasCallEnded.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
callState.addSource(
|
||||
outgoingCallPlaced,
|
||||
placed -> {
|
||||
if (Boolean.TRUE.equals(placed) && !callCoordinator.isIncomingCall()) {
|
||||
if (callState.getValue() == CallState.CONNECTING) {
|
||||
callState.setValue(CallState.RINGING);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
callState.addSource(
|
||||
answeredElsewhere,
|
||||
value -> {
|
||||
if (Boolean.TRUE.equals(value)) {
|
||||
hasCallEnded.set(true);
|
||||
callState.setValue(CallState.ANSWERED_ELSEWHERE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CallState translateConnectionState(PeerConnection.PeerConnectionState state) {
|
||||
switch (state) {
|
||||
case NEW:
|
||||
if (callCoordinator.isIncomingCall()) {
|
||||
return CallState.PROMPTING_USER_ACCEPT;
|
||||
} else {
|
||||
return CallState.RINGING;
|
||||
}
|
||||
|
||||
case CONNECTING:
|
||||
if (callCoordinator.isIncomingCall()) {
|
||||
return CallState.CONNECTING;
|
||||
} else {
|
||||
return Boolean.TRUE.equals(outgoingCallPlaced.getValue())
|
||||
? CallState.RINGING
|
||||
: CallState.CONNECTING;
|
||||
}
|
||||
|
||||
case CONNECTED:
|
||||
return CallState.CONNECTED;
|
||||
|
||||
case DISCONNECTED:
|
||||
return CallState.RECONNECTING;
|
||||
|
||||
case FAILED:
|
||||
return CallState.ERROR;
|
||||
|
||||
case CLOSED:
|
||||
return CallState.ENDED;
|
||||
|
||||
default:
|
||||
return CallState.INITIALIZING;
|
||||
}
|
||||
}
|
||||
|
||||
// Call Control
|
||||
|
||||
public void answerCall() {
|
||||
Log.d(TAG, "answerCall");
|
||||
|
||||
if (!callCoordinator.isIncomingCall()) {
|
||||
Log.w(TAG, "answerCall() called but this is not an incoming call");
|
||||
return;
|
||||
}
|
||||
|
||||
// System integration
|
||||
callCoordinator.handleCallControlScopeAnswer();
|
||||
|
||||
answerCallWhenReady();
|
||||
}
|
||||
|
||||
/** Answer incoming call (WebRTC only) Used when system answer already happened */
|
||||
public void answerCallWhenReady() {
|
||||
Log.d(TAG, "answerCallWhenReady");
|
||||
|
||||
if (callCoordinator.hasOngoingCall()) {
|
||||
Log.w(TAG, "Call already ongoing, not starting duplicate");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start media capture
|
||||
callCoordinator.startMediaCapture();
|
||||
|
||||
// Create one-time observer
|
||||
LiveData<VideoTrack> localTrack = callCoordinator.getLocalVideoTrack();
|
||||
|
||||
answerCallObserver =
|
||||
new Observer<VideoTrack>() {
|
||||
@Override
|
||||
public void onChanged(VideoTrack videoTrack) {
|
||||
if (videoTrack != null) {
|
||||
// Media is ready, remove observer
|
||||
localTrack.removeObserver(this);
|
||||
answerCallObserver = null;
|
||||
|
||||
Log.d(TAG, "Local video ready, answering call (WebRTC)");
|
||||
|
||||
callCoordinator.answerWebRTC();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
localTrack.observeForever(answerCallObserver);
|
||||
}
|
||||
|
||||
/** Start outgoing call with media capture Called by Activity for outgoing calls */
|
||||
public void startOutgoingCallWhenReady() {
|
||||
Log.d(TAG, "startOutgoingCallWhenReady");
|
||||
|
||||
if (callCoordinator.hasOngoingCall()) {
|
||||
Log.w(TAG, "Call already ongoing, not starting duplicate");
|
||||
return;
|
||||
}
|
||||
|
||||
callCoordinator.startMediaCapture();
|
||||
|
||||
// Create one-time observer
|
||||
LiveData<VideoTrack> localTrack = callCoordinator.getLocalVideoTrack();
|
||||
VideoTrack currentValue = localTrack.getValue();
|
||||
|
||||
if (currentValue != null) {
|
||||
Log.d(TAG, "Media already ready, starting call immediately");
|
||||
callCoordinator.startOutgoingCall();
|
||||
} else {
|
||||
startOutgoingCallObserver =
|
||||
new Observer<VideoTrack>() {
|
||||
@Override
|
||||
public void onChanged(VideoTrack videoTrack) {
|
||||
if (videoTrack != null) {
|
||||
// Media is ready, remove observer
|
||||
localTrack.removeObserver(this);
|
||||
startOutgoingCallObserver = null;
|
||||
|
||||
Log.d(TAG, "Local video ready, starting outgoing call");
|
||||
|
||||
callCoordinator.startOutgoingCall();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
localTrack.observeForever(startOutgoingCallObserver);
|
||||
}
|
||||
}
|
||||
|
||||
public void declineCall() {
|
||||
Log.d(TAG, "declineCall");
|
||||
|
||||
if (!hasCallEnded.compareAndSet(false, true)) {
|
||||
Log.w(TAG, "Call already ended");
|
||||
return;
|
||||
}
|
||||
|
||||
callCoordinator.declineCall();
|
||||
}
|
||||
|
||||
public void hangUp() {
|
||||
Log.d(TAG, "hangUp");
|
||||
|
||||
if (!hasCallEnded.compareAndSet(false, true)) {
|
||||
Log.w(TAG, "Call already ended");
|
||||
return;
|
||||
}
|
||||
|
||||
callCoordinator.hangUp();
|
||||
}
|
||||
|
||||
public void toggleAudio() {
|
||||
Boolean current = localAudioEnabled.getValue();
|
||||
boolean newState = current == null || !current;
|
||||
|
||||
Log.d(TAG, "toggleAudio: " + newState);
|
||||
|
||||
callCoordinator.setAudioEnabled(newState);
|
||||
}
|
||||
|
||||
public void toggleVideo() {
|
||||
Boolean current = localVideoEnabled.getValue();
|
||||
boolean newState = current == null || !current;
|
||||
|
||||
Log.d(TAG, "toggleVideo: " + newState);
|
||||
|
||||
callCoordinator.setVideoEnabled(newState);
|
||||
}
|
||||
|
||||
public void switchCamera() {
|
||||
Log.d(TAG, "switchCamera");
|
||||
|
||||
callCoordinator.switchCamera();
|
||||
}
|
||||
|
||||
public void selectAudioDevice(CallEndpointCompat endpoint) {
|
||||
Log.d(TAG, "selectAudioDevice: " + endpoint.getName());
|
||||
|
||||
callCoordinator.requestAudioEndpointChange(endpoint);
|
||||
}
|
||||
|
||||
public void switchToEarpiece(boolean shallUseEarpiece) {
|
||||
Log.d(TAG, "switchToEarpiece: shallUseEarpiece: " + shallUseEarpiece);
|
||||
|
||||
List<CallEndpointCompat> endpoints = availableAudioEndpoints.getValue();
|
||||
|
||||
if (endpoints == null) {
|
||||
Log.w(TAG, "No available audio endpoint");
|
||||
return;
|
||||
}
|
||||
|
||||
CallEndpointCompat targetEndpoint = null;
|
||||
for (CallEndpointCompat endpoint : endpoints) {
|
||||
boolean isEarpiece = endpoint.getType() == CallEndpointCompat.TYPE_EARPIECE;
|
||||
if (isEarpiece == shallUseEarpiece) {
|
||||
targetEndpoint = endpoint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetEndpoint != null) {
|
||||
selectAudioDevice(targetEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
public void setStartsWithVideo(boolean startsWithVideo) {
|
||||
Log.d(TAG, "setStartsWithVideo: " + startsWithVideo);
|
||||
callCoordinator.setStartsWithVideo(startsWithVideo);
|
||||
}
|
||||
|
||||
// CallControlScope Callbacks
|
||||
|
||||
public void onCallAnswered() {
|
||||
Log.d(TAG, "onCallAnswered callback from CallControlScope");
|
||||
}
|
||||
|
||||
public void onCallActive() {
|
||||
Log.d(TAG, "onCallActive callback from CallControlScope");
|
||||
}
|
||||
|
||||
public void onCallInactive() {
|
||||
Log.d(TAG, "onCallInactive callback from CallControlScope");
|
||||
}
|
||||
|
||||
public void onCallDisconnected(DisconnectCause disconnectCause) {
|
||||
Log.d(TAG, "onCallDisconnected callback from CallControlScope, cause: " + disconnectCause);
|
||||
|
||||
if (hasCallEnded.compareAndSet(false, true)) {
|
||||
callState.postValue(CallState.ENDED);
|
||||
}
|
||||
}
|
||||
|
||||
// LiveData Getters
|
||||
|
||||
public LiveData<CallState> getCallState() {
|
||||
return callState;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getAudioEnabled() {
|
||||
return localAudioEnabled;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getVideoEnabled() {
|
||||
return localVideoEnabled;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getRemoteAudioEnabled() {
|
||||
return remoteAudioEnabled;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getRemoteVideoEnabled() {
|
||||
return remoteVideoEnabled;
|
||||
}
|
||||
|
||||
public LiveData<VideoTrack> getLocalVideoTrack() {
|
||||
return localVideoTrack;
|
||||
}
|
||||
|
||||
public LiveData<VideoTrack> getRemoteVideoTrack() {
|
||||
return remoteVideoTrack;
|
||||
}
|
||||
|
||||
public LiveData<String> getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getIsRelayUsed() {
|
||||
return isRelayUsed;
|
||||
}
|
||||
|
||||
public LiveData<String> getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public LiveData<Icon> getDisplayIcon() {
|
||||
return displayIcon;
|
||||
}
|
||||
|
||||
public LiveData<CallEndpointCompat> getCurrentAudioEndpoint() {
|
||||
return currentAudioEndpoint;
|
||||
}
|
||||
|
||||
public LiveData<List<CallEndpointCompat>> getAvailableAudioEndpoints() {
|
||||
return availableAudioEndpoints;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getIsFrontCamera() {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
// Notification Action Handlers
|
||||
|
||||
public void handleNotificationAnswer() {
|
||||
Log.d(TAG, "handleNotificationAnswer");
|
||||
|
||||
answerCall();
|
||||
}
|
||||
|
||||
public void handleNotificationDecline() {
|
||||
Log.d(TAG, "handleNotificationDecline");
|
||||
|
||||
declineCall();
|
||||
}
|
||||
|
||||
public void handleNotificationHangup() {
|
||||
Log.d(TAG, "handleNotificationHangup");
|
||||
|
||||
hangUp();
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
Log.d(TAG, "CallViewModel cleared");
|
||||
|
||||
if (answerCallObserver != null) {
|
||||
callCoordinator.getLocalVideoTrack().removeObserver(answerCallObserver);
|
||||
answerCallObserver = null;
|
||||
}
|
||||
|
||||
if (startOutgoingCallObserver != null) {
|
||||
callCoordinator.getLocalVideoTrack().removeObserver(startOutgoingCallObserver);
|
||||
startOutgoingCallObserver = null;
|
||||
}
|
||||
|
||||
callCoordinator.clearActiveCallViewModel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package org.thoughtcrime.securesms.calls;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import org.thoughtcrime.securesms.EglUtils;
|
||||
import org.webrtc.AudioSource;
|
||||
import org.webrtc.AudioTrack;
|
||||
import org.webrtc.Camera2Enumerator;
|
||||
import org.webrtc.CameraVideoCapturer;
|
||||
import org.webrtc.MediaConstraints;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.SurfaceTextureHelper;
|
||||
import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoSource;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
public class MediaStreamManager {
|
||||
|
||||
private static final String TAG = MediaStreamManager.class.getSimpleName();
|
||||
private static final String STREAM_ID = "local_stream";
|
||||
private static final String AUDIO_TRACK_ID = "audio_track";
|
||||
private static final String VIDEO_TRACK_ID = "video_track";
|
||||
|
||||
private final Context context;
|
||||
private final PeerConnectionFactory peerConnectionFactory;
|
||||
|
||||
private VideoCapturer videoCapturer;
|
||||
private VideoSource videoSource;
|
||||
private AudioSource audioSource;
|
||||
private SurfaceTextureHelper surfaceTextureHelper;
|
||||
private volatile boolean isFrontCamera = true;
|
||||
|
||||
public interface Callback {
|
||||
void onMediaStreamReady(MediaStream stream);
|
||||
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public interface CameraSwitchCallback {
|
||||
void onCameraSwitch(boolean isFrontCamera);
|
||||
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public MediaStreamManager(@NonNull Context context, PeerConnectionFactory peerConnectionFactory) {
|
||||
this.context = context.getApplicationContext();
|
||||
|
||||
this.peerConnectionFactory = peerConnectionFactory;
|
||||
}
|
||||
|
||||
/** Create media stream with audio and optionally video */
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void createMediaStream(Callback callback) {
|
||||
try {
|
||||
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream(STREAM_ID);
|
||||
|
||||
// Create audio track
|
||||
MediaConstraints audioConstraints = new MediaConstraints();
|
||||
audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
|
||||
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());
|
||||
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) {
|
||||
Log.e(TAG, "Failed to create media stream", e);
|
||||
callback.onError("Failed to access camera/microphone: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private VideoCapturer createVideoCapturer() {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
Log.w(TAG, "Camera permission not granted");
|
||||
return null;
|
||||
}
|
||||
|
||||
Camera2Enumerator enumerator = new Camera2Enumerator(context);
|
||||
|
||||
// Try front camera first
|
||||
String[] deviceNames = enumerator.getDeviceNames();
|
||||
for (String deviceName : deviceNames) {
|
||||
if (enumerator.isFrontFacing(deviceName)) {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
isFrontCamera = true;
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to any camera
|
||||
for (String deviceName : deviceNames) {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
isFrontCamera = enumerator.isFrontFacing(deviceName);
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void switchCamera(@Nullable CameraSwitchCallback callback) {
|
||||
if (!(videoCapturer instanceof CameraVideoCapturer)) {
|
||||
Log.e(TAG, "switchCamera called but videoCapturer is not a CameraVideoCapturer");
|
||||
return;
|
||||
}
|
||||
|
||||
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
|
||||
|
||||
// Find the opposite-facing camera
|
||||
Camera2Enumerator enumerator = new Camera2Enumerator(context);
|
||||
String[] deviceNames = enumerator.getDeviceNames();
|
||||
|
||||
String targetCameraName = null;
|
||||
for (String deviceName : deviceNames) {
|
||||
boolean isTargetFront = !isFrontCamera;
|
||||
boolean deviceIsFront = enumerator.isFrontFacing(deviceName);
|
||||
|
||||
if (deviceIsFront == isTargetFront) {
|
||||
targetCameraName = deviceName;
|
||||
break; // Take the first match
|
||||
}
|
||||
}
|
||||
|
||||
if (targetCameraName == null) {
|
||||
Log.e(TAG, "No camera found with opposite facing direction");
|
||||
if (callback != null) {
|
||||
callback.onError("No opposite camera available");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final String finalTargetCameraName = targetCameraName;
|
||||
Log.d(TAG, "Switching to camera: " + finalTargetCameraName);
|
||||
|
||||
// Call with explicit camera name
|
||||
cameraVideoCapturer.switchCamera(
|
||||
new CameraVideoCapturer.CameraSwitchHandler() {
|
||||
@Override
|
||||
public void onCameraSwitchDone(boolean isFront) {
|
||||
Log.d(TAG, "switchCamera SUCCESS, isFront=" + isFront);
|
||||
isFrontCamera = isFront;
|
||||
if (callback != null) callback.onCameraSwitch(isFront);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraSwitchError(String errorDescription) {
|
||||
Log.e(TAG, "switchCamera FAILED: " + errorDescription);
|
||||
if (callback != null) callback.onError(errorDescription);
|
||||
}
|
||||
},
|
||||
finalTargetCameraName);
|
||||
}
|
||||
|
||||
public boolean isFrontCamera() {
|
||||
return isFrontCamera;
|
||||
}
|
||||
|
||||
/** Cleanup resources */
|
||||
public void dispose() {
|
||||
if (videoCapturer != null) {
|
||||
try {
|
||||
videoCapturer.stopCapture();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Error stopping capture", e);
|
||||
}
|
||||
videoCapturer.dispose();
|
||||
videoCapturer = null;
|
||||
}
|
||||
|
||||
if (surfaceTextureHelper != null) {
|
||||
surfaceTextureHelper.dispose();
|
||||
surfaceTextureHelper = null;
|
||||
}
|
||||
|
||||
if (videoSource != null) {
|
||||
videoSource.dispose();
|
||||
videoSource = null;
|
||||
}
|
||||
|
||||
if (audioSource != null) {
|
||||
audioSource.dispose();
|
||||
audioSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
@@ -18,6 +19,7 @@ import java.util.List;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class StickerPickerView extends RecyclerView {
|
||||
|
||||
@@ -130,25 +132,26 @@ public class StickerPickerView extends RecyclerView {
|
||||
|
||||
private void deleteSticker(File stickerFile) {
|
||||
if (stickerFile != null && stickerFile.exists()) {
|
||||
new androidx.appcompat.app.AlertDialog.Builder(context)
|
||||
.setTitle(R.string.delete)
|
||||
.setMessage(R.string.ask_delete_sticker)
|
||||
.setPositiveButton(
|
||||
R.string.delete,
|
||||
(dialog, which) -> {
|
||||
if (stickerFile.delete()) {
|
||||
int position = stickerFiles.indexOf(stickerFile);
|
||||
if (position >= 0) {
|
||||
stickerFiles.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onStickerDeleted(stickerFile);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
AlertDialog dialog =
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(R.string.ask_delete_sticker)
|
||||
.setPositiveButton(
|
||||
R.string.delete,
|
||||
(d, which) -> {
|
||||
if (stickerFile.delete()) {
|
||||
int position = stickerFiles.indexOf(stickerFile);
|
||||
if (position >= 0) {
|
||||
stickerFiles.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onStickerDeleted(stickerFile);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
Util.redPositiveButton(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,24 @@ package org.thoughtcrime.securesms.connect;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
public class AttachmentsContentProvider extends ContentProvider {
|
||||
|
||||
private static final String[] ALL_COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
||||
|
||||
/* We save all attachments in our private files-directory
|
||||
that cannot be read by other apps.
|
||||
|
||||
@@ -46,7 +52,8 @@ public class AttachmentsContentProvider extends ContentProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri arg0, String arg1, String[] arg2) {
|
||||
public int delete(
|
||||
@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -55,7 +62,7 @@ public class AttachmentsContentProvider extends ContentProvider {
|
||||
String file = uri.getPathSegments().get(0);
|
||||
String mimeType = DcHelper.sharedFiles.get(file);
|
||||
|
||||
return DcHelper.checkMime(uri.toString(), mimeType);
|
||||
return DcHelper.checkMime(uri.getPathSegments().get(1), mimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,22 +72,69 @@ public class AttachmentsContentProvider extends ContentProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri arg0, ContentValues arg1) {
|
||||
public Uri insert(@NonNull Uri arg0, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) {
|
||||
return null;
|
||||
public Cursor query(
|
||||
@NonNull Uri uri,
|
||||
@Nullable String[] projection,
|
||||
@Nullable String selection,
|
||||
@Nullable String[] selectionArgs,
|
||||
@Nullable String sortOrder) {
|
||||
// Segment [0] is the blob file key ("ef39a39"), same as in openFile() and getType()
|
||||
// Segment [1] is the original display name ("text.txt")
|
||||
String file = uri.getPathSegments().get(0);
|
||||
|
||||
// Same guard used in openFile()
|
||||
if (!DcHelper.sharedFiles.containsKey(file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Resolve the actual File
|
||||
DcContext dcContext = DcHelper.getContext(Objects.requireNonNull(getContext()));
|
||||
File privateFile = new File(dcContext.getBlobdir(), file);
|
||||
|
||||
// Default to all supported columns
|
||||
if (projection == null) {
|
||||
projection = ALL_COLUMNS;
|
||||
}
|
||||
|
||||
// Build column-name and value arrays
|
||||
String[] cols = new String[projection.length];
|
||||
Object[] values = new Object[projection.length];
|
||||
int i = 0;
|
||||
for (String col : projection) {
|
||||
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||
cols[i] = OpenableColumns.DISPLAY_NAME;
|
||||
values[i++] = uri.getPathSegments().get(1);
|
||||
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||
cols[i] = OpenableColumns.SIZE;
|
||||
values[i++] = privateFile.length();
|
||||
}
|
||||
}
|
||||
|
||||
// Set arrays to only matched columns.
|
||||
cols = Arrays.copyOf(cols, i);
|
||||
values = Arrays.copyOf(values, i);
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(cols, 1);
|
||||
cursor.addRow(values);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
|
||||
public int update(
|
||||
@NonNull Uri uri,
|
||||
@Nullable ContentValues values,
|
||||
@Nullable String selection,
|
||||
@Nullable String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,125 +3,128 @@ package org.thoughtcrime.securesms.connect;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Hashtable;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.service.FetchForegroundService;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Hashtable;
|
||||
|
||||
public class DcEventCenter {
|
||||
private static final String TAG = DcEventCenter.class.getSimpleName();
|
||||
private @NonNull final Hashtable<Integer, ArrayList<DcEventDelegate>> currentAccountObservers = new Hashtable<>();
|
||||
private @NonNull final Hashtable<Integer, ArrayList<DcEventDelegate>> multiAccountObservers = new Hashtable<>();
|
||||
private final Object LOCK = new Object();
|
||||
private final @NonNull ApplicationContext context;
|
||||
private static final String TAG = DcEventCenter.class.getSimpleName();
|
||||
private @NonNull final Hashtable<Integer, ArrayList<DcEventDelegate>> currentAccountObservers =
|
||||
new Hashtable<>();
|
||||
private @NonNull final Hashtable<Integer, ArrayList<DcEventDelegate>> multiAccountObservers =
|
||||
new Hashtable<>();
|
||||
private final Object LOCK = new Object();
|
||||
private final @NonNull ApplicationContext context;
|
||||
|
||||
public interface DcEventDelegate {
|
||||
void handleEvent(@NonNull DcEvent event);
|
||||
default boolean runOnMain() {
|
||||
return true;
|
||||
public interface DcEventDelegate {
|
||||
void handleEvent(@NonNull DcEvent event);
|
||||
|
||||
default boolean runOnMain() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public DcEventCenter(@NonNull Context context) {
|
||||
this.context = ApplicationContext.getInstance(context);
|
||||
}
|
||||
|
||||
public void addObserver(int eventId, @NonNull DcEventDelegate observer) {
|
||||
addObserver(currentAccountObservers, eventId, observer);
|
||||
}
|
||||
|
||||
public void addMultiAccountObserver(int eventId, @NonNull DcEventDelegate observer) {
|
||||
addObserver(multiAccountObservers, eventId, observer);
|
||||
}
|
||||
|
||||
private void addObserver(
|
||||
Hashtable<Integer, ArrayList<DcEventDelegate>> observers,
|
||||
int eventId,
|
||||
@NonNull DcEventDelegate observer) {
|
||||
synchronized (LOCK) {
|
||||
ArrayList<DcEventDelegate> idObservers = observers.get(eventId);
|
||||
if (idObservers == null) {
|
||||
observers.put(eventId, (idObservers = new ArrayList<>()));
|
||||
}
|
||||
idObservers.add(observer);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeObserver(int eventId, DcEventDelegate observer) {
|
||||
synchronized (LOCK) {
|
||||
ArrayList<DcEventDelegate> idObservers = currentAccountObservers.get(eventId);
|
||||
if (idObservers != null) {
|
||||
idObservers.remove(observer);
|
||||
}
|
||||
idObservers = multiAccountObservers.get(eventId);
|
||||
if (idObservers != null) {
|
||||
idObservers.remove(observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeObservers(DcEventDelegate observer) {
|
||||
synchronized (LOCK) {
|
||||
for (Integer eventId : currentAccountObservers.keySet()) {
|
||||
ArrayList<DcEventDelegate> idObservers = currentAccountObservers.get(eventId);
|
||||
if (idObservers != null) {
|
||||
idObservers.remove(observer);
|
||||
}
|
||||
}
|
||||
|
||||
public DcEventCenter(@NonNull Context context) {
|
||||
this.context = ApplicationContext.getInstance(context);
|
||||
}
|
||||
|
||||
public void addObserver(int eventId, @NonNull DcEventDelegate observer) {
|
||||
addObserver(currentAccountObservers, eventId, observer);
|
||||
}
|
||||
|
||||
public void addMultiAccountObserver(int eventId, @NonNull DcEventDelegate observer) {
|
||||
addObserver(multiAccountObservers, eventId, observer);
|
||||
}
|
||||
|
||||
private void addObserver(Hashtable<Integer, ArrayList<DcEventDelegate>> observers, int eventId, @NonNull DcEventDelegate observer) {
|
||||
synchronized (LOCK) {
|
||||
ArrayList<DcEventDelegate> idObservers = observers.get(eventId);
|
||||
if (idObservers == null) {
|
||||
observers.put(eventId, (idObservers = new ArrayList<>()));
|
||||
}
|
||||
idObservers.add(observer);
|
||||
}
|
||||
for (Integer eventId : multiAccountObservers.keySet()) {
|
||||
ArrayList<DcEventDelegate> idObservers = multiAccountObservers.get(eventId);
|
||||
if (idObservers != null) {
|
||||
idObservers.remove(observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeObserver(int eventId, DcEventDelegate observer) {
|
||||
synchronized (LOCK) {
|
||||
ArrayList<DcEventDelegate> idObservers = currentAccountObservers.get(eventId);
|
||||
if (idObservers != null) {
|
||||
idObservers.remove(observer);
|
||||
}
|
||||
idObservers = multiAccountObservers.get(eventId);
|
||||
if (idObservers != null) {
|
||||
idObservers.remove(observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeObservers(DcEventDelegate observer) {
|
||||
synchronized (LOCK) {
|
||||
for(Integer eventId : currentAccountObservers.keySet()) {
|
||||
ArrayList<DcEventDelegate> idObservers = currentAccountObservers.get(eventId);
|
||||
if (idObservers != null) {
|
||||
idObservers.remove(observer);
|
||||
}
|
||||
}
|
||||
for(Integer eventId : multiAccountObservers.keySet()) {
|
||||
ArrayList<DcEventDelegate> idObservers = multiAccountObservers.get(eventId);
|
||||
if (idObservers != null) {
|
||||
idObservers.remove(observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendToMultiAccountObservers(@NonNull DcEvent event) {
|
||||
sendToObservers(multiAccountObservers, event);
|
||||
}
|
||||
|
||||
private void sendToCurrentAccountObservers(@NonNull DcEvent event) {
|
||||
sendToObservers(currentAccountObservers, event);
|
||||
}
|
||||
|
||||
private void sendToObservers(Hashtable<Integer, ArrayList<DcEventDelegate>> observers, @NonNull DcEvent event) {
|
||||
synchronized (LOCK) {
|
||||
ArrayList<DcEventDelegate> idObservers = observers.get(event.getId());
|
||||
if (idObservers != null) {
|
||||
for (DcEventDelegate observer : idObservers) {
|
||||
// using try/catch blocks as under some circumstances eg. getContext() may return NULL -
|
||||
// and as this function is used virtually everywhere, also in libs,
|
||||
// it's not feasible to check all single occurrences.
|
||||
if(observer.runOnMain()) {
|
||||
Util.runOnMain(() -> {
|
||||
try {
|
||||
observer.handleEvent(event);
|
||||
}
|
||||
catch(Exception e) {
|
||||
Log.e(TAG, "Error calling observer.handleEvent()", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Util.runOnBackground(() -> {
|
||||
try {
|
||||
observer.handleEvent(event);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.e(TAG, "Error calling observer.handleEvent()", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
private void sendToMultiAccountObservers(@NonNull DcEvent event) {
|
||||
sendToObservers(multiAccountObservers, event);
|
||||
}
|
||||
|
||||
private void sendToCurrentAccountObservers(@NonNull DcEvent event) {
|
||||
sendToObservers(currentAccountObservers, event);
|
||||
}
|
||||
|
||||
private void sendToObservers(
|
||||
Hashtable<Integer, ArrayList<DcEventDelegate>> observers, @NonNull DcEvent event) {
|
||||
synchronized (LOCK) {
|
||||
ArrayList<DcEventDelegate> idObservers = observers.get(event.getId());
|
||||
if (idObservers != null) {
|
||||
for (DcEventDelegate observer : idObservers) {
|
||||
// using try/catch blocks as under some circumstances eg. getContext() may return NULL -
|
||||
// and as this function is used virtually everywhere, also in libs,
|
||||
// it's not feasible to check all single occurrences.
|
||||
if (observer.runOnMain()) {
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
try {
|
||||
observer.handleEvent(event);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error calling observer.handleEvent()", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Util.runOnBackground(
|
||||
() -> {
|
||||
try {
|
||||
observer.handleEvent(event);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error calling observer.handleEvent()", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Object lastErrorLock = new Object();
|
||||
private boolean showNextErrorAsToast = true;
|
||||
@@ -148,24 +151,26 @@ public class DcEventCenter {
|
||||
}
|
||||
|
||||
// show error to user
|
||||
Util.runOnMain(() -> {
|
||||
if (showAsToast) {
|
||||
String toastString = null;
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
if (showAsToast) {
|
||||
String toastString = null;
|
||||
|
||||
if (event == DcContext.DC_EVENT_ERROR_SELF_NOT_IN_GROUP) {
|
||||
toastString = context.getString(R.string.group_self_not_in_group);
|
||||
}
|
||||
if (event == DcContext.DC_EVENT_ERROR_SELF_NOT_IN_GROUP) {
|
||||
toastString = context.getString(R.string.group_self_not_in_group);
|
||||
}
|
||||
|
||||
ForegroundDetector foregroundDetector = ForegroundDetector.getInstance();
|
||||
if (toastString != null && (foregroundDetector == null || foregroundDetector.isForeground())) {
|
||||
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
ForegroundDetector foregroundDetector = ForegroundDetector.getInstance();
|
||||
if (toastString != null
|
||||
&& (foregroundDetector == null || foregroundDetector.isForeground())) {
|
||||
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void handleLogging(@NonNull DcEvent event) {
|
||||
final String logPrefix = "[accId="+event.getAccountId()+ "] ";
|
||||
final String logPrefix = "[accId=" + event.getAccountId() + "] ";
|
||||
switch (event.getId()) {
|
||||
case DcContext.DC_EVENT_INFO:
|
||||
Log.i("DeltaChat", logPrefix + event.getData2Str());
|
||||
@@ -189,24 +194,19 @@ public class DcEventCenter {
|
||||
|
||||
switch (id) {
|
||||
case DcContext.DC_EVENT_INCOMING_MSG:
|
||||
DcHelper.getNotificationCenter(context).notifyMessage(accountId, event.getData1Int(), event.getData2Int());
|
||||
DcHelper.getNotificationCenter(context)
|
||||
.notifyMessage(accountId, event.getData1Int(), event.getData2Int());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_INCOMING_REACTION:
|
||||
DcHelper.getNotificationCenter(context).notifyReaction(accountId, event.getData1Int(), event.getData2Int(), event.getData2Str());
|
||||
DcHelper.getNotificationCenter(context)
|
||||
.notifyReaction(
|
||||
accountId, event.getData1Int(), event.getData2Int(), event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_INCOMING_WEBXDC_NOTIFY:
|
||||
DcHelper.getNotificationCenter(context).notifyWebxdc(accountId, event.getData1Int(), event.getData2Int(), event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_INCOMING_CALL:
|
||||
DcHelper.getNotificationCenter(context).notifyCall(accountId, event.getData1Int(), event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_INCOMING_CALL_ACCEPTED:
|
||||
case DcContext.DC_EVENT_CALL_ENDED:
|
||||
DcHelper.getNotificationCenter(context).removeCallNotification(accountId, event.getData1Int());
|
||||
DcHelper.getNotificationCenter(context)
|
||||
.notifyWebxdc(accountId, event.getData1Int(), event.getData2Int(), event.getData2Str());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_MSGS_NOTICED:
|
||||
@@ -214,7 +214,8 @@ public class DcEventCenter {
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_MSG_DELETED:
|
||||
DcHelper.getNotificationCenter(context).removeNotification(accountId, event.getData1Int(), event.getData2Int());
|
||||
DcHelper.getNotificationCenter(context)
|
||||
.removeNotification(accountId, event.getData1Int(), event.getData2Int());
|
||||
break;
|
||||
|
||||
case DcContext.DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE:
|
||||
|
||||
@@ -12,17 +12,19 @@ import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import com.b44t.messenger.DcAccounts;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcLot;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.LocalHelpActivity;
|
||||
@@ -38,83 +40,79 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class DcHelper {
|
||||
|
||||
private static final String TAG = DcHelper.class.getSimpleName();
|
||||
private static final String TAG = DcHelper.class.getSimpleName();
|
||||
|
||||
public static final String CONFIG_CONFIGURED_ADDRESS = "configured_addr";
|
||||
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_CONFIGURED_ADDRESS = "configured_addr";
|
||||
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 DcContext getContext(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcContext();
|
||||
}
|
||||
public static DcContext getContext(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcContext();
|
||||
}
|
||||
|
||||
public static Rpc getRpc(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getRpc();
|
||||
}
|
||||
public static Rpc getRpc(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getRpc();
|
||||
}
|
||||
|
||||
public static DcAccounts getAccounts(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcAccounts();
|
||||
}
|
||||
public static DcAccounts getAccounts(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcAccounts();
|
||||
}
|
||||
|
||||
public static DcEventCenter getEventCenter(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getEventCenter();
|
||||
}
|
||||
public static DcEventCenter getEventCenter(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getEventCenter();
|
||||
}
|
||||
|
||||
public static NotificationCenter getNotificationCenter(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getNotificationCenter();
|
||||
}
|
||||
public static NotificationCenter getNotificationCenter(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getNotificationCenter();
|
||||
}
|
||||
|
||||
public static boolean isConfigured(Context context) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.isConfigured() == 1;
|
||||
}
|
||||
public static boolean isConfigured(Context context) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.isConfigured() == 1;
|
||||
}
|
||||
|
||||
public static int getInt(Context context, String key) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.getConfigInt(key);
|
||||
}
|
||||
public static int getInt(Context context, String key) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.getConfigInt(key);
|
||||
}
|
||||
|
||||
public static String get(Context context, String key) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.getConfig(key);
|
||||
}
|
||||
public static String get(Context context, String key) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.getConfig(key);
|
||||
}
|
||||
|
||||
@Deprecated public static int getInt(Context context, String key, int defaultValue) {
|
||||
return getInt(context, key);
|
||||
}
|
||||
@Deprecated
|
||||
public static int getInt(Context context, String key, int defaultValue) {
|
||||
return getInt(context, key);
|
||||
}
|
||||
|
||||
@Deprecated public static String get(Context context, String key, String defaultValue) {
|
||||
return get(context, key);
|
||||
}
|
||||
@Deprecated
|
||||
public static String get(Context context, String key, String defaultValue) {
|
||||
return get(context, key);
|
||||
}
|
||||
|
||||
public static void set(Context context, String key, String value) {
|
||||
DcContext dcContext = getContext(context);
|
||||
dcContext.setConfig(key, value);
|
||||
}
|
||||
public static void set(Context context, String key, String value) {
|
||||
DcContext dcContext = getContext(context);
|
||||
dcContext.setConfig(key, value);
|
||||
}
|
||||
|
||||
public static void setStockTranslations(Context context) {
|
||||
DcContext dcContext = getContext(context);
|
||||
// the integers are defined in the core and used only here, an enum or sth. like that won't have a big benefit
|
||||
// the integers are defined in the core and used only here, an enum or sth. like that won't have
|
||||
// a big benefit
|
||||
dcContext.setStockTranslation(1, context.getString(R.string.chat_no_messages));
|
||||
dcContext.setStockTranslation(2, context.getString(R.string.self));
|
||||
dcContext.setStockTranslation(3, context.getString(R.string.draft));
|
||||
@@ -132,8 +130,13 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(68, context.getString(R.string.device_talk));
|
||||
dcContext.setStockTranslation(69, context.getString(R.string.saved_messages));
|
||||
dcContext.setStockTranslation(70, context.getString(R.string.device_talk_explain));
|
||||
dcContext.setStockTranslation(71, context.getString(R.string.device_welcome_message, "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=3KvvQZfzU4t-9u5s0PF3USGp&i=X-QDZ681F6Plz_uBu47CKdg4&s=IwE4LraLDcdPBW597wB7DBnI&a=arcanechat%40arcanechat.me&n=ArcaneChat&g=ArcaneChat+Community"));
|
||||
dcContext.setStockTranslation(73, context.getString(R.string.systemmsg_subject_for_new_contact));
|
||||
dcContext.setStockTranslation(
|
||||
71,
|
||||
context.getString(
|
||||
R.string.device_welcome_message,
|
||||
"https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=AM1YCd_2hKuiSiTEb-2177On&a=arcanechat%40arcanechat.me&n=ArcaneChat&b=ArcaneChat+Channel"));
|
||||
dcContext.setStockTranslation(
|
||||
73, context.getString(R.string.systemmsg_subject_for_new_contact));
|
||||
dcContext.setStockTranslation(74, context.getString(R.string.systemmsg_failed_sending_to));
|
||||
dcContext.setStockTranslation(84, context.getString(R.string.configuration_failed_with_error));
|
||||
dcContext.setStockTranslation(85, context.getString(R.string.devicemsg_bad_time));
|
||||
@@ -142,8 +145,6 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(91, context.getString(R.string.devicemsg_self_deleted));
|
||||
dcContext.setStockTranslation(97, context.getString(R.string.forwarded));
|
||||
dcContext.setStockTranslation(98, context.getString(R.string.devicemsg_storage_exceeding));
|
||||
dcContext.setStockTranslation(99, context.getString(R.string.n_bytes_message));
|
||||
dcContext.setStockTranslation(100, context.getString(R.string.download_max_available_until));
|
||||
dcContext.setStockTranslation(103, context.getString(R.string.incoming_messages));
|
||||
dcContext.setStockTranslation(104, context.getString(R.string.outgoing_messages));
|
||||
dcContext.setStockTranslation(107, context.getString(R.string.connectivity_connected));
|
||||
@@ -160,9 +161,11 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(119, context.getString(R.string.qrshow_join_contact_hint));
|
||||
|
||||
// HACK: svg does not handle entities correctly and shows `"` as the text `quot;`.
|
||||
// until that is fixed, we fix the most obvious errors (core uses encode_minimal, so this does not affect so many characters)
|
||||
// until that is fixed, we fix the most obvious errors (core uses encode_minimal, so this does
|
||||
// not affect so many characters)
|
||||
// cmp. https://github.com/deltachat/deltachat-android/issues/2187
|
||||
dcContext.setStockTranslation(120, context.getString(R.string.qrshow_join_group_hint).replace("\"", ""));
|
||||
dcContext.setStockTranslation(
|
||||
120, context.getString(R.string.qrshow_join_group_hint).replace("\"", ""));
|
||||
dcContext.setStockTranslation(121, context.getString(R.string.connectivity_not_connected));
|
||||
dcContext.setStockTranslation(124, context.getString(R.string.group_name_changed_by_you));
|
||||
dcContext.setStockTranslation(125, context.getString(R.string.group_name_changed_by_other));
|
||||
@@ -179,9 +182,11 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(136, context.getString(R.string.location_enabled_by_you));
|
||||
dcContext.setStockTranslation(137, context.getString(R.string.location_enabled_by_other));
|
||||
dcContext.setStockTranslation(138, context.getString(R.string.ephemeral_timer_disabled_by_you));
|
||||
dcContext.setStockTranslation(139, context.getString(R.string.ephemeral_timer_disabled_by_other));
|
||||
dcContext.setStockTranslation(
|
||||
139, context.getString(R.string.ephemeral_timer_disabled_by_other));
|
||||
dcContext.setStockTranslation(140, context.getString(R.string.ephemeral_timer_seconds_by_you));
|
||||
dcContext.setStockTranslation(141, context.getString(R.string.ephemeral_timer_seconds_by_other));
|
||||
dcContext.setStockTranslation(
|
||||
141, context.getString(R.string.ephemeral_timer_seconds_by_other));
|
||||
dcContext.setStockTranslation(144, context.getString(R.string.ephemeral_timer_1_hour_by_you));
|
||||
dcContext.setStockTranslation(145, context.getString(R.string.ephemeral_timer_1_hour_by_other));
|
||||
dcContext.setStockTranslation(146, context.getString(R.string.ephemeral_timer_1_day_by_you));
|
||||
@@ -189,7 +194,8 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(148, context.getString(R.string.ephemeral_timer_1_week_by_you));
|
||||
dcContext.setStockTranslation(149, context.getString(R.string.ephemeral_timer_1_week_by_other));
|
||||
dcContext.setStockTranslation(150, context.getString(R.string.ephemeral_timer_minutes_by_you));
|
||||
dcContext.setStockTranslation(151, context.getString(R.string.ephemeral_timer_minutes_by_other));
|
||||
dcContext.setStockTranslation(
|
||||
151, context.getString(R.string.ephemeral_timer_minutes_by_other));
|
||||
dcContext.setStockTranslation(152, context.getString(R.string.ephemeral_timer_hours_by_you));
|
||||
dcContext.setStockTranslation(153, context.getString(R.string.ephemeral_timer_hours_by_other));
|
||||
dcContext.setStockTranslation(154, context.getString(R.string.ephemeral_timer_days_by_you));
|
||||
@@ -199,11 +205,14 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(158, context.getString(R.string.ephemeral_timer_1_year_by_you));
|
||||
dcContext.setStockTranslation(159, context.getString(R.string.ephemeral_timer_1_year_by_other));
|
||||
dcContext.setStockTranslation(162, context.getString(R.string.multidevice_qr_subtitle));
|
||||
dcContext.setStockTranslation(163, context.getString(R.string.multidevice_transfer_done_devicemsg));
|
||||
dcContext.setStockTranslation(170, context.getString(R.string.chat_protection_enabled_tap_to_learn_more));
|
||||
dcContext.setStockTranslation(
|
||||
163, context.getString(R.string.multidevice_transfer_done_devicemsg));
|
||||
dcContext.setStockTranslation(
|
||||
170, context.getString(R.string.chat_protection_enabled_tap_to_learn_more));
|
||||
dcContext.setStockTranslation(172, context.getString(R.string.chat_new_group_hint));
|
||||
dcContext.setStockTranslation(173, context.getString(R.string.member_x_added));
|
||||
dcContext.setStockTranslation(174, context.getString(R.string.invalid_unencrypted_tap_to_learn_more));
|
||||
dcContext.setStockTranslation(
|
||||
174, context.getString(R.string.invalid_unencrypted_tap_to_learn_more));
|
||||
dcContext.setStockTranslation(176, context.getString(R.string.reaction_by_you));
|
||||
dcContext.setStockTranslation(177, context.getString(R.string.reaction_by_other));
|
||||
dcContext.setStockTranslation(178, context.getString(R.string.member_x_removed));
|
||||
@@ -225,28 +234,35 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(234, context.getString(R.string.audio_call));
|
||||
dcContext.setStockTranslation(235, context.getString(R.string.video_call));
|
||||
dcContext.setStockTranslation(240, context.getString(R.string.chat_description_changed_by_you));
|
||||
dcContext.setStockTranslation(241, context.getString(R.string.chat_description_changed_by_other));
|
||||
dcContext.setStockTranslation(
|
||||
241, context.getString(R.string.chat_description_changed_by_other));
|
||||
dcContext.setStockTranslation(242, context.getString(R.string.messages_are_e2ee));
|
||||
}
|
||||
|
||||
public static File getImexDir() {
|
||||
// DIRECTORY_DOCUMENTS could be used but DIRECTORY_DOWNLOADS seems to be easier accessible by the user,
|
||||
// DIRECTORY_DOCUMENTS could be used but DIRECTORY_DOWNLOADS seems to be easier accessible by
|
||||
// the user,
|
||||
// eg. "Download Managers" are nearly always installed.
|
||||
// CAVE: do not use DownloadManager to add the file as it is deleted on uninstall then ...
|
||||
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
}
|
||||
|
||||
// When the user shares a file to another app or opens a file in another app, it is added here.
|
||||
// `HashMap<file, mimetype>` where `file` is the name of the file in the blobdir (not the user-visible filename).
|
||||
// `HashMap<file, mimetype>` where `file` is the name of the file in the blobdir (not the
|
||||
// user-visible filename).
|
||||
public static final HashMap<String, String> sharedFiles = new HashMap<>();
|
||||
|
||||
public static void openForViewOrShare(Context activity, int msg_id, String cmd) {
|
||||
DcContext dcContext = getContext(activity);
|
||||
|
||||
if(!(activity instanceof Activity)) {
|
||||
if (!(activity instanceof Activity)) {
|
||||
// would be nicer to accepting only Activity objects,
|
||||
// however, typically in Android just Context objects are passed around (as this normally does not make a difference).
|
||||
// Accepting only Activity here would force callers to cast, which would easily result in even more ugliness.
|
||||
Toast.makeText(activity, "openForViewOrShare() expects an Activity object", Toast.LENGTH_LONG).show();
|
||||
// however, typically in Android just Context objects are passed around (as this normally does
|
||||
// not make a difference).
|
||||
// Accepting only Activity here would force callers to cast, which would easily result in even
|
||||
// more ugliness.
|
||||
Toast.makeText(activity, "openForViewOrShare() expects an Activity object", Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -257,7 +273,9 @@ public class DcHelper {
|
||||
try {
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
Toast.makeText(activity, activity.getString(R.string.file_not_found, path), Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(
|
||||
activity, activity.getString(R.string.file_not_found, path), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,7 +285,14 @@ public class DcHelper {
|
||||
// Build a Uri that will later be passed to AttachmentsContentProvider.openFile().
|
||||
// The last part needs to be `filename`, i.e. the original, user-visible name of the file,
|
||||
// so that the external apps show the name of the file correctly.
|
||||
uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(file.getName()) + "/" + Uri.encode(filename));
|
||||
uri =
|
||||
Uri.parse(
|
||||
"content://"
|
||||
+ BuildConfig.APPLICATION_ID
|
||||
+ ".attachments/"
|
||||
+ Uri.encode(file.getName())
|
||||
+ "/"
|
||||
+ Uri.encode(filename));
|
||||
|
||||
// As different Android version handle uris in putExtra differently,
|
||||
// we also check on our own that the file was actually shared.
|
||||
@@ -275,7 +300,9 @@ public class DcHelper {
|
||||
sharedFiles.put(file.getName(), mimeType);
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileprovider", file);
|
||||
uri =
|
||||
FileProvider.getUriForFile(
|
||||
activity, BuildConfig.APPLICATION_ID + ".fileprovider", file);
|
||||
} else {
|
||||
uri = Uri.fromFile(file);
|
||||
}
|
||||
@@ -292,87 +319,108 @@ public class DcHelper {
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, msg.getText());
|
||||
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.chat_share_with_title)));
|
||||
activity.startActivity(
|
||||
Intent.createChooser(intent, activity.getString(R.string.chat_share_with_title)));
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Toast.makeText(activity, String.format("%s (%s)", activity.getString(R.string.no_app_to_handle_data), mimeType), Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(
|
||||
activity,
|
||||
String.format(
|
||||
"%s (%s)", activity.getString(R.string.no_app_to_handle_data), mimeType),
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.i(TAG, "opening of external activity failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendToChat(Context activity, byte[] data, String type, String fileName, String html, String subject, String text) {
|
||||
Intent intent = new Intent(activity, ShareActivity.class);
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
ShareUtil.setIsFromWebxdc(intent, true);
|
||||
public static void sendToChat(
|
||||
Context activity,
|
||||
byte[] data,
|
||||
String type,
|
||||
String fileName,
|
||||
String html,
|
||||
String subject,
|
||||
String text) {
|
||||
Intent intent = new Intent(activity, ShareActivity.class);
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
ShareUtil.setIsFromWebxdc(intent, true);
|
||||
|
||||
if (data != null) {
|
||||
String mimeType = "application/octet-stream";
|
||||
Uri uri = PersistentBlobProvider.getInstance().create(activity, data, mimeType, fileName);
|
||||
intent.setType(mimeType);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
ShareUtil.setSharedType(intent, type);
|
||||
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_file_to, fileName));
|
||||
} else {
|
||||
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_message_to));
|
||||
}
|
||||
if (data != null) {
|
||||
String mimeType = "application/octet-stream";
|
||||
Uri uri = PersistentBlobProvider.getInstance().create(activity, data, mimeType, fileName);
|
||||
intent.setType(mimeType);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
ShareUtil.setSharedType(intent, type);
|
||||
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_file_to, fileName));
|
||||
} else {
|
||||
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_message_to));
|
||||
}
|
||||
|
||||
if (text != null) {
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text);
|
||||
}
|
||||
if (text != null) {
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text);
|
||||
}
|
||||
|
||||
if (subject != null) {
|
||||
ShareUtil.setSharedSubject(intent, subject);
|
||||
}
|
||||
if (subject != null) {
|
||||
ShareUtil.setSharedSubject(intent, subject);
|
||||
}
|
||||
|
||||
if (html != null) {
|
||||
String mimeType = "application/octet-stream";
|
||||
Uri uri = PersistentBlobProvider.getInstance().create(activity, html.getBytes(), mimeType, "index.html");
|
||||
ShareUtil.setSharedHtml(intent, uri);
|
||||
}
|
||||
if (html != null) {
|
||||
String mimeType = "application/octet-stream";
|
||||
Uri uri =
|
||||
PersistentBlobProvider.getInstance()
|
||||
.create(activity, html.getBytes(), mimeType, "index.html");
|
||||
ShareUtil.setSharedHtml(intent, uri);
|
||||
}
|
||||
|
||||
activity.startActivity(intent);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
private static void startActivity(Activity activity, Intent intent) {
|
||||
// request for permission to install apks on API 26+ if intent mimetype is an apk
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
"application/vnd.android.package-archive".equals(intent.getType()) &&
|
||||
!activity.getPackageManager().canRequestPackageInstalls()) {
|
||||
activity.startActivity(new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(Uri.parse(String.format("package:%s", activity.getPackageName()))));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
&& "application/vnd.android.package-archive".equals(intent.getType())
|
||||
&& !activity.getPackageManager().canRequestPackageInstalls()) {
|
||||
activity.startActivity(
|
||||
new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||
.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))));
|
||||
return;
|
||||
}
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
public static String checkMime(String path, String mimeType) {
|
||||
if(mimeType == null || mimeType.equals("application/octet-stream")) {
|
||||
if (mimeType == null || mimeType.equals("application/octet-stream")) {
|
||||
path = path.replaceAll(" ", "");
|
||||
String extension = MediaUtil.getFileExtensionFromUrl(path);
|
||||
String newType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if(newType != null) return newType;
|
||||
if (newType != null) return newType;
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path of a not-yet-existing file in the blobdir with roughly the given filename
|
||||
* and the given extension.
|
||||
* In many cases, since we're using setFileAndDeduplicate now, this wouldn't be necessary anymore
|
||||
* and we could just create a file with a random filename,
|
||||
* but there are a few usages that still need the current behavior (like `openMaps()`).
|
||||
* Return the path of a not-yet-existing file in the blobdir with roughly the given filename and
|
||||
* the given extension. In many cases, since we're using setFileAndDeduplicate now, this wouldn't
|
||||
* be necessary anymore and we could just create a file with a random filename, but there are a
|
||||
* few usages that still need the current behavior (like `openMaps()`).
|
||||
*/
|
||||
public static String getBlobdirFile(DcContext dcContext, String filename, String ext) {
|
||||
filename = FileUtils.sanitizeFilename(filename);
|
||||
ext = FileUtils.sanitizeFilename(ext);
|
||||
String outPath = null;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
String test = dcContext.getBlobdir() + "/" + filename + (i == 0 ? "" : i < 100 ? "-" + i : "-" + (new Date().getTime() + i)) + ext;
|
||||
String test =
|
||||
dcContext.getBlobdir()
|
||||
+ "/"
|
||||
+ filename
|
||||
+ (i == 0 ? "" : i < 100 ? "-" + i : "-" + (new Date().getTime() + i))
|
||||
+ ext;
|
||||
if (!new File(test).exists()) {
|
||||
outPath = test;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(outPath==null) {
|
||||
if (outPath == null) {
|
||||
// should not happen
|
||||
outPath = dcContext.getBlobdir() + "/" + Math.random();
|
||||
}
|
||||
@@ -380,19 +428,22 @@ public class DcHelper {
|
||||
}
|
||||
|
||||
public static String getBlobdirFile(DcContext dcContext, String path) {
|
||||
String filename = path.substring(path.lastIndexOf('/')+1); // is the whole path if '/' is not found (lastIndexOf() returns -1 then)
|
||||
String filename =
|
||||
path.substring(
|
||||
path.lastIndexOf('/')
|
||||
+ 1); // is the whole path if '/' is not found (lastIndexOf() returns -1 then)
|
||||
String ext = "";
|
||||
int point = filename.indexOf('.');
|
||||
if(point!=-1) {
|
||||
if (point != -1) {
|
||||
ext = filename.substring(point);
|
||||
filename = filename.substring(0, point);
|
||||
}
|
||||
return getBlobdirFile(dcContext, filename, ext);
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ThreadRecord getThreadRecord(Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
|
||||
public static ThreadRecord getThreadRecord(
|
||||
Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
|
||||
int chatId = chat.getId();
|
||||
|
||||
String body = summary.getText1();
|
||||
@@ -405,14 +456,23 @@ public class DcHelper {
|
||||
long date = summary.getTimestamp();
|
||||
int unreadCount = getContext(context).getFreshMsgCount(chatId);
|
||||
|
||||
return new ThreadRecord(body, recipient, date,
|
||||
unreadCount, chatId,
|
||||
chat.getVisibility(), chat.isSendingLocations(), chat.isMuted(), chat.isContactRequest(), summary);
|
||||
return new ThreadRecord(
|
||||
body,
|
||||
recipient,
|
||||
date,
|
||||
unreadCount,
|
||||
chatId,
|
||||
chat.getVisibility(),
|
||||
chat.isSendingLocations(),
|
||||
chat.isMuted(),
|
||||
chat.isContactRequest(),
|
||||
summary);
|
||||
}
|
||||
|
||||
public static boolean isNetworkConnected(Context context) {
|
||||
try {
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
ConnectivityManager cm =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo netInfo = cm.getActiveNetworkInfo();
|
||||
if (netInfo != null && netInfo.isConnected()) {
|
||||
return true;
|
||||
@@ -425,105 +485,110 @@ public class DcHelper {
|
||||
|
||||
/**
|
||||
* Gets a string you can show to the user with basic information about connectivity.
|
||||
*
|
||||
* @param context
|
||||
* @param connectedString Usually "Connected", but when using this as the title in
|
||||
* ConversationListActivity, we want to write "ArcaneChat"
|
||||
* or the user's display name there instead.
|
||||
* ConversationListActivity, we want to write "Delta Chat" or the user's display name there
|
||||
* instead.
|
||||
* @return
|
||||
*/
|
||||
public static String getConnectivitySummary(Context context, String connectedString) {
|
||||
int connectivity = getContext(context).getConnectivity();
|
||||
if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTED) {
|
||||
return connectedString;
|
||||
} else if (connectivity >= DcContext.DC_CONNECTIVITY_WORKING) {
|
||||
return context.getString(R.string.connectivity_updating);
|
||||
} else if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTING) {
|
||||
return context.getString(R.string.connectivity_connecting);
|
||||
} else {
|
||||
return context.getString(R.string.connectivity_not_connected);
|
||||
}
|
||||
int connectivity = getContext(context).getConnectivity();
|
||||
if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTED) {
|
||||
return connectedString;
|
||||
} else if (connectivity >= DcContext.DC_CONNECTIVITY_WORKING) {
|
||||
return context.getString(R.string.connectivity_updating);
|
||||
} else if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTING) {
|
||||
return context.getString(R.string.connectivity_connecting);
|
||||
} else {
|
||||
return context.getString(R.string.connectivity_not_connected);
|
||||
}
|
||||
}
|
||||
|
||||
public static void showProtectionEnabledDialog(Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.chat_protection_enabled_explanation))
|
||||
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#e2ee"))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
.setMessage(context.getString(R.string.chat_protection_enabled_explanation))
|
||||
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#e2ee"))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void showInvalidUnencryptedDialog(Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
.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();
|
||||
.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();
|
||||
}
|
||||
|
||||
public static void openHelp(Context context, String section) {
|
||||
Intent intent = new Intent(context, LocalHelpActivity.class);
|
||||
if (section != null) { intent.putExtra(LocalHelpActivity.SECTION_EXTRA, section); }
|
||||
if (section != null) {
|
||||
intent.putExtra(LocalHelpActivity.SECTION_EXTRA, section);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* For the PGP-Contacts migration, things can go wrong.
|
||||
* The migration happens when the account is setup, at which point no events can be sent yet.
|
||||
* So, instead, if something goes wrong, it's returned by getLastError().
|
||||
* This function shows the error message to the user.
|
||||
* <p>
|
||||
* A few releases after the PGP-contacts migration (which happened in 2025-05),
|
||||
* we can remove this function again.
|
||||
*/
|
||||
public static void maybeShowMigrationError(Context context) {
|
||||
try {
|
||||
String lastError = DcHelper.getRpc(context).getMigrationError(DcHelper.getContext(context).getAccountId());
|
||||
/**
|
||||
* For the PGP-Contacts migration, things can go wrong. The migration happens when the account is
|
||||
* setup, at which point no events can be sent yet. So, instead, if something goes wrong, it's
|
||||
* returned by getLastError(). This function shows the error message to the user.
|
||||
*
|
||||
* <p>A few releases after the PGP-contacts migration (which happened in 2025-05), we can remove
|
||||
* this function again.
|
||||
*/
|
||||
public static void maybeShowMigrationError(Context context) {
|
||||
try {
|
||||
String lastError =
|
||||
DcHelper.getRpc(context).getMigrationError(DcHelper.getContext(context).getAccountId());
|
||||
|
||||
if (lastError != null && !lastError.isEmpty()) {
|
||||
Log.w(TAG, "Opening account failed, trying to share error: " + lastError);
|
||||
if (lastError != null && !lastError.isEmpty()) {
|
||||
Log.w(TAG, "Opening account failed, trying to share error: " + lastError);
|
||||
|
||||
String subject = "Delta Chat failed to update";
|
||||
String email = "delta@merlinux.eu";
|
||||
String subject = "Delta Chat failed to update";
|
||||
String email = "delta@merlinux.eu";
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.error_x, lastError))
|
||||
.setNeutralButton(R.string.global_menu_edit_copy_desktop, (d, which) -> {
|
||||
Util.writeTextToClipboard(context, lastError);
|
||||
})
|
||||
.setPositiveButton(R.string.menu_send, (d, which) -> {
|
||||
Intent sharingIntent = new Intent(
|
||||
Intent.ACTION_SENDTO, Uri.fromParts(
|
||||
"mailto", email, null
|
||||
)
|
||||
);
|
||||
sharingIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{email});
|
||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.error_x, lastError))
|
||||
.setNeutralButton(
|
||||
R.string.global_menu_edit_copy_desktop,
|
||||
(d, which) -> {
|
||||
Util.writeTextToClipboard(context, lastError);
|
||||
})
|
||||
.setPositiveButton(
|
||||
R.string.menu_send,
|
||||
(d, which) -> {
|
||||
Intent sharingIntent =
|
||||
new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", email, null));
|
||||
sharingIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {email});
|
||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
|
||||
|
||||
if (sharingIntent.resolveActivity(context.getPackageManager()) == null) {
|
||||
Log.w(TAG, "No email client found to send crash report");
|
||||
sharingIntent = new Intent(Intent.ACTION_SEND);
|
||||
sharingIntent.setType("text/plain");
|
||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
|
||||
sharingIntent.putExtra(Intent.EXTRA_EMAIL, email);
|
||||
}
|
||||
if (sharingIntent.resolveActivity(context.getPackageManager()) == null) {
|
||||
Log.w(TAG, "No email client found to send crash report");
|
||||
sharingIntent = new Intent(Intent.ACTION_SEND);
|
||||
sharingIntent.setType("text/plain");
|
||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
|
||||
sharingIntent.putExtra(Intent.EXTRA_EMAIL, email);
|
||||
}
|
||||
|
||||
Intent chooser =
|
||||
Intent.createChooser(sharingIntent, "Send using...");
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
|
||||
Intent chooser = Intent.createChooser(sharingIntent, "Send using...");
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
|
||||
|
||||
context.startActivity(chooser);
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
context.startActivity(chooser);
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.thoughtcrime.securesms.notifications;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class DeclineCallReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = DeclineCallReceiver.class.getSimpleName();
|
||||
|
||||
public static final String DECLINE_ACTION = "org.thoughtcrime.securesms.notifications.DECLINE_CALL";
|
||||
public static final String ACCOUNT_ID_EXTRA = "account_id";
|
||||
public static final String CALL_ID_EXTRA = "call_id";
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, Intent intent) {
|
||||
if (!DECLINE_ACTION.equals(intent.getAction())) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int accountId = intent.getIntExtra(ACCOUNT_ID_EXTRA, 0);
|
||||
final int callId = intent.getIntExtra(CALL_ID_EXTRA, 0);
|
||||
if (accountId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
DcHelper.getNotificationCenter(context).removeCallNotification(accountId, callId);
|
||||
try {
|
||||
DcHelper.getRpc(context).endCall(accountId, callId);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "Error", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -66,6 +66,71 @@ public class PersistentBlobProvider {
|
||||
|
||||
private PersistentBlobProvider() {}
|
||||
|
||||
public Uri create(
|
||||
@NonNull Context context,
|
||||
@NonNull File inputFile,
|
||||
@NonNull String mimeType,
|
||||
@Nullable String fileName,
|
||||
@Nullable Long fileSize)
|
||||
throws IOException {
|
||||
|
||||
if (!inputFile.exists()) {
|
||||
throw new IOException("Input file does not exist: " + inputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
final long id = System.currentTimeMillis();
|
||||
|
||||
if (fileName == null) {
|
||||
fileName = "file." + getExtensionFromMimeType(mimeType);
|
||||
}
|
||||
|
||||
if (fileSize == null) {
|
||||
fileSize = inputFile.length();
|
||||
}
|
||||
|
||||
File destFile = getModernCacheFile(context, id);
|
||||
|
||||
// Try move first
|
||||
boolean moved = inputFile.renameTo(destFile);
|
||||
|
||||
if (!moved) {
|
||||
// Move failed, copy instead
|
||||
InputStream input = null;
|
||||
OutputStream output = null;
|
||||
try {
|
||||
input = new FileInputStream(inputFile);
|
||||
output = new FileOutputStream(destFile);
|
||||
Util.copy(input, output);
|
||||
} finally {
|
||||
if (output != null) {
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
if (input != null) {
|
||||
try {
|
||||
input.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Uri uniqueUri =
|
||||
CONTENT_URI
|
||||
.buildUpon()
|
||||
.appendPath(mimeType)
|
||||
.appendPath(fileName)
|
||||
.appendEncodedPath(String.valueOf(fileSize))
|
||||
.appendEncodedPath(String.valueOf(System.currentTimeMillis()))
|
||||
.build();
|
||||
|
||||
return ContentUris.withAppendedId(uniqueUri, id);
|
||||
}
|
||||
|
||||
public Uri create(
|
||||
@NonNull Context context,
|
||||
@NonNull byte[] blobBytes,
|
||||
@@ -143,7 +208,6 @@ public class PersistentBlobProvider {
|
||||
return getFile(context, ContentUris.parseId(uri)).delete();
|
||||
}
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (isExternalBlobUri(context, uri)) {
|
||||
return new File(uri.getPath()).delete();
|
||||
}
|
||||
@@ -222,6 +286,20 @@ public class PersistentBlobProvider {
|
||||
|
||||
private static @NonNull File getExternalDir(Context context) throws IOException {
|
||||
File externalDir = context.getExternalCacheDir();
|
||||
|
||||
if (externalDir != null) {
|
||||
try {
|
||||
FileProviderUtil.getUriFor(context, new File(externalDir, "test"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"External cache dir not resolvable by FileProvider, "
|
||||
+ "falling back to internal cache",
|
||||
e);
|
||||
externalDir = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (externalDir == null) {
|
||||
externalDir = context.getCacheDir();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.relay;
|
||||
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_ENABLED;
|
||||
import static org.thoughtcrime.securesms.connect.DcHelper.getContext;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
@@ -24,7 +23,6 @@ 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;
|
||||
@@ -44,7 +42,6 @@ import org.thoughtcrime.securesms.WelcomeActivity;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.proxy.ProxySettingsActivity;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -79,8 +76,6 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
Spinner smtpSecurity;
|
||||
Spinner certCheck;
|
||||
|
||||
private SwitchCompat proxySwitch;
|
||||
|
||||
Rpc rpc;
|
||||
int accId;
|
||||
|
||||
@@ -116,13 +111,6 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
smtpSecurity = findViewById(R.id.smtp_security);
|
||||
certCheck = findViewById(R.id.cert_check);
|
||||
|
||||
proxySwitch = findViewById(R.id.proxy_settings);
|
||||
proxySwitch.setOnClickListener(
|
||||
l -> {
|
||||
proxySwitch.setChecked(!proxySwitch.isChecked()); // revert toggle
|
||||
startActivity(new Intent(this, ProxySettingsActivity.class));
|
||||
});
|
||||
|
||||
String addr = getIntent().getStringExtra(EXTRA_ADDR);
|
||||
EnteredLoginParam config = null;
|
||||
try {
|
||||
@@ -185,10 +173,6 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
boolean expandAdvanced = false;
|
||||
int intVal;
|
||||
|
||||
intVal = DcHelper.getInt(this, CONFIG_PROXY_ENABLED);
|
||||
proxySwitch.setChecked(intVal == 1);
|
||||
expandAdvanced = expandAdvanced || intVal == 1;
|
||||
|
||||
if (config != null) { // configured
|
||||
emailInput.setText(config.addr);
|
||||
if (!TextUtils.isEmpty(config.addr)) {
|
||||
@@ -246,7 +230,6 @@ public class EditRelayActivity extends BaseActionBarActivity
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1);
|
||||
}
|
||||
|
||||
private void showLog() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ScrollView;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -16,7 +17,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import chat.delta.rpc.types.EnteredLoginParam;
|
||||
import chat.delta.rpc.types.TransportListEntry;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.google.zxing.integration.android.IntentIntegrator;
|
||||
@@ -53,7 +54,7 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
private ActivityResultLauncher<Intent> qrScannerLauncher;
|
||||
|
||||
/** Relay selected for context menu via onRelayLongClick() */
|
||||
private EnteredLoginParam contextMenuRelay = null;
|
||||
private TransportListEntry contextMenuRelay = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -99,7 +100,8 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
PulsingFloatingActionButton fabAdd = findViewById(R.id.fab_add_relay);
|
||||
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(recyclerView);
|
||||
ScrollView scrollView = findViewById(R.id.relay_scroll_view);
|
||||
ViewUtil.applyWindowInsets(scrollView);
|
||||
// Apply insets to prevent fab from being covered by system bars
|
||||
ViewUtil.applyWindowInsetsAsMargin(fabAdd);
|
||||
|
||||
@@ -162,7 +164,7 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
String finalMainRelayAddr = mainRelayAddr;
|
||||
|
||||
try {
|
||||
List<EnteredLoginParam> relays = rpc.listTransports(accId);
|
||||
List<TransportListEntry> relays = rpc.listTransportsEx(accId);
|
||||
|
||||
Util.runOnMain(() -> adapter.setRelays(relays, finalMainRelayAddr));
|
||||
} catch (RpcException e) {
|
||||
@@ -173,12 +175,12 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelayClick(EnteredLoginParam relay) {
|
||||
if (relay.addr != null && !relay.addr.equals(adapter.getMainRelay())) {
|
||||
public void onRelayClick(TransportListEntry relay) {
|
||||
if (relay.param.addr != null && !relay.param.addr.equals(adapter.getMainRelay())) {
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
try {
|
||||
rpc.setConfig(accId, DcHelper.CONFIG_CONFIGURED_ADDRESS, relay.addr);
|
||||
rpc.setConfig(accId, DcHelper.CONFIG_CONFIGURED_ADDRESS, relay.param.addr);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC.setConfig() failed", e);
|
||||
}
|
||||
@@ -189,7 +191,7 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelayLongClick(View view, EnteredLoginParam relay) {
|
||||
public void onRelayLongClick(View view, TransportListEntry relay) {
|
||||
contextMenuRelay = relay;
|
||||
registerForContextMenu(view);
|
||||
openContextMenu(view);
|
||||
@@ -201,8 +203,10 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
getMenuInflater().inflate(R.menu.relay_item_context, menu);
|
||||
|
||||
boolean nonNullAddr = contextMenuRelay != null && contextMenuRelay.addr != null;
|
||||
boolean isMain = nonNullAddr && contextMenuRelay.addr.equals(adapter.getMainRelay());
|
||||
boolean nonNullAddr = contextMenuRelay != null && contextMenuRelay.param.addr != null;
|
||||
boolean isMain = nonNullAddr && contextMenuRelay.param.addr.equals(adapter.getMainRelay());
|
||||
|
||||
Util.redMenuItem(menu, R.id.menu_delete_relay);
|
||||
menu.findItem(R.id.menu_delete_relay).setVisible(!isMain);
|
||||
}
|
||||
|
||||
@@ -230,28 +234,40 @@ public class RelayListActivity extends BaseActionBarActivity
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
private void onRelayEdit(EnteredLoginParam relay) {
|
||||
private void onRelayEdit(TransportListEntry relay) {
|
||||
Intent intent = new Intent(this, EditRelayActivity.class);
|
||||
intent.putExtra(EditRelayActivity.EXTRA_ADDR, relay.addr);
|
||||
intent.putExtra(EditRelayActivity.EXTRA_ADDR, relay.param.addr);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void onRelayDelete(EnteredLoginParam relay) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.remove_transport)
|
||||
.setMessage(getString(R.string.confirm_remove_transport, relay.addr))
|
||||
.setPositiveButton(
|
||||
R.string.ok,
|
||||
(dialog, which) -> {
|
||||
try {
|
||||
rpc.deleteTransport(accId, relay.addr);
|
||||
loadRelays();
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC.deleteTransport() failed", e);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
private void onRelayDelete(TransportListEntry relay) {
|
||||
AlertDialog dialog =
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.remove_transport)
|
||||
.setMessage(getString(R.string.confirm_remove_or_hide_transport_x, relay.param.addr))
|
||||
.setPositiveButton(
|
||||
R.string.remove_transport,
|
||||
(d, which) -> {
|
||||
try {
|
||||
rpc.deleteTransport(accId, relay.param.addr);
|
||||
loadRelays();
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "RPC.deleteTransport() failed", e);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(
|
||||
R.string.hide_from_contacts,
|
||||
(d, which) -> {
|
||||
try {
|
||||
rpc.setTransportUnpublished(accId, relay.param.addr, true);
|
||||
loadRelays();
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "cannot unpublish relay: ", e);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
Util.redPositiveButton(dialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.relay;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -8,21 +9,21 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import chat.delta.rpc.types.EnteredLoginParam;
|
||||
import chat.delta.rpc.types.TransportListEntry;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.RelayViewHolder> {
|
||||
|
||||
private List<EnteredLoginParam> relays = new ArrayList<>();
|
||||
private List<TransportListEntry> relays = new ArrayList<>();
|
||||
private final OnRelayClickListener listener;
|
||||
private String mainRelayAddr;
|
||||
|
||||
public interface OnRelayClickListener {
|
||||
void onRelayClick(EnteredLoginParam relay);
|
||||
void onRelayClick(TransportListEntry relay);
|
||||
|
||||
void onRelayLongClick(View view, EnteredLoginParam relay);
|
||||
void onRelayLongClick(View view, TransportListEntry relay);
|
||||
}
|
||||
|
||||
public RelayListAdapter(OnRelayClickListener listener) {
|
||||
@@ -33,7 +34,7 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
|
||||
return mainRelayAddr;
|
||||
}
|
||||
|
||||
public void setRelays(@Nullable List<EnteredLoginParam> relays, String mainRelayAddr) {
|
||||
public void setRelays(@Nullable List<TransportListEntry> relays, String mainRelayAddr) {
|
||||
this.relays = relays != null ? relays : new ArrayList<>();
|
||||
this.mainRelayAddr = mainRelayAddr;
|
||||
notifyDataSetChanged();
|
||||
@@ -49,8 +50,8 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RelayViewHolder holder, int position) {
|
||||
EnteredLoginParam relay = relays.get(position);
|
||||
boolean isMain = relay.addr != null && relay.addr.equals(mainRelayAddr);
|
||||
TransportListEntry relay = relays.get(position);
|
||||
boolean isMain = relay.param.addr != null && relay.param.addr.equals(mainRelayAddr);
|
||||
holder.bind(relay, isMain, listener);
|
||||
}
|
||||
|
||||
@@ -71,10 +72,20 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
|
||||
mainIndicator = itemView.findViewById(R.id.main_indicator);
|
||||
}
|
||||
|
||||
public void bind(EnteredLoginParam relay, boolean isMain, OnRelayClickListener listener) {
|
||||
String[] parts = relay.addr.split("@");
|
||||
public void bind(TransportListEntry relay, boolean isMain, OnRelayClickListener listener) {
|
||||
Context context = itemView.getContext();
|
||||
String[] parts = relay.param.addr.split("@");
|
||||
titleText.setText(parts.length == 2 ? parts[1] : parts[0]);
|
||||
subtitleText.setText(parts.length == 2 ? parts[0] : "");
|
||||
|
||||
String subtitle = parts.length == 2 ? parts[0] : "";
|
||||
if (isMain) {
|
||||
subtitle += " · " + context.getString(R.string.used_for_sending);
|
||||
}
|
||||
if (relay.isUnpublished) {
|
||||
subtitle += " · " + context.getString(R.string.hidden_from_contacts);
|
||||
}
|
||||
subtitleText.setText(subtitle);
|
||||
|
||||
mainIndicator.setVisibility(isMain ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
itemView.setOnClickListener(
|
||||
|
||||
@@ -2,45 +2,46 @@ package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Spannable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
public class MarkdownUtil {
|
||||
private static MarkdownUtil instance;
|
||||
private final Markwon markwon;
|
||||
|
||||
private MarkdownUtil(final Context context) {
|
||||
markwon = Markwon.builder(context)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(MarkwonInlineParserPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
registry.require(MarkwonInlineParserPlugin.class, plugin -> {
|
||||
plugin.factoryBuilder().excludeInlineProcessor(HtmlInlineProcessor.class);
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.enabledBlockTypes(new HashSet<>(Collections.singletonList(FencedCodeBlock.class)));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
markwon =
|
||||
Markwon.builder(context)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(MarkwonInlineParserPlugin.create())
|
||||
.usePlugin(
|
||||
new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
registry.require(
|
||||
MarkwonInlineParserPlugin.class,
|
||||
plugin -> {
|
||||
plugin.factoryBuilder().excludeInlineProcessor(HtmlInlineProcessor.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.enabledBlockTypes(
|
||||
new HashSet<>(Collections.singletonList(FencedCodeBlock.class)));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private static MarkdownUtil getInstance(Context context) {
|
||||
|
||||
@@ -41,6 +41,7 @@ public class MediaUtil {
|
||||
public static final String IMAGE_JPEG = "image/jpeg";
|
||||
public static final String IMAGE_GIF = "image/gif";
|
||||
public static final String AUDIO_AAC = "audio/aac";
|
||||
public static final String AUDIO_M4A = "audio/mp4";
|
||||
public static final String AUDIO_UNSPECIFIED = "audio/*";
|
||||
public static final String VIDEO_UNSPECIFIED = "video/*";
|
||||
public static final String OCTET = "application/octet-stream";
|
||||
@@ -294,6 +295,8 @@ public class MediaUtil {
|
||||
switch (contentType) {
|
||||
case AUDIO_AAC:
|
||||
return "aac";
|
||||
case AUDIO_M4A:
|
||||
return "m4a";
|
||||
case IMAGE_WEBP:
|
||||
return "webp";
|
||||
case WEBXDC:
|
||||
|
||||
@@ -198,10 +198,6 @@ public class Prefs {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isCallsEnabled(Context context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ringtone
|
||||
|
||||
public static @NonNull Uri getNotificationRingtone(Context context) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M680,480L680,328L588,420L560,392L672,280L560,168L588,140L680,232L680,80L700,80L814,196L728,280L814,366L700,480L680,480ZM720,404L758,366L720,328L720,404ZM720,232L758,196L720,158L720,232ZM798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM241,360L307,294Q307,294 307,294Q307,294 307,294L290,200Q290,200 290,200Q290,200 290,200L201,200Q201,200 201,200Q201,200 201,200Q206,241 215,281Q224,321 241,360ZM599,718Q638,735 678.5,745Q719,755 760,758Q760,758 760,758Q760,758 760,758L760,670Q760,670 760,670Q760,670 760,670L666,651Q666,651 666,651Q666,651 666,651L599,718ZM241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360ZM599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM241,360L307,294Q307,294 307,294Q307,294 307,294L290,200Q290,200 290,200Q290,200 290,200L201,200Q201,200 201,200Q201,200 201,200Q206,241 215,281Q224,321 241,360ZM599,718Q638,735 678.5,745Q719,755 760,758Q760,758 760,758Q760,758 760,758L760,670Q760,670 760,670Q760,670 760,670L666,651Q666,651 666,651Q666,651 666,651L599,718ZM241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360ZM599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M136,656L44,566Q32,554 32,538Q32,522 44,510Q132,415 247,367.5Q362,320 480,320Q598,320 712.5,367.5Q827,415 916,510Q928,522 928,538Q928,554 916,566L824,656Q813,667 798.5,668Q784,669 772,660L656,572Q648,566 644,558Q640,550 640,540L640,426Q602,414 562,407Q522,400 480,400Q438,400 398,407Q358,414 320,426L320,540Q320,550 316,558Q312,566 304,572L188,660Q176,669 161.5,668Q147,667 136,656ZM240,458Q211,473 184,492.5Q157,512 128,536Q128,536 128,536Q128,536 128,536L168,576Q168,576 168,576Q168,576 168,576L240,520Q240,520 240,520Q240,520 240,520L240,458ZM720,460L720,520Q720,520 720,520Q720,520 720,520L792,576Q792,576 792,576Q792,576 792,576L832,538Q832,538 832,538Q832,538 832,538Q803,512 776,493Q749,474 720,460ZM240,458L240,458Q240,458 240,458Q240,458 240,458L240,458Q240,458 240,458Q240,458 240,458L240,458Q240,458 240,458Q240,458 240,458Q240,458 240,458Q240,458 240,458ZM720,460Q720,460 720,460Q720,460 720,460Q720,460 720,460Q720,460 720,460L720,460Q720,460 720,460Q720,460 720,460L720,460Q720,460 720,460Q720,460 720,460Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480ZM800,800L600,800Q600,780 598.5,760Q597,740 594,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,286Q140,283 120,281.5Q100,280 80,280L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800ZM80,800L80,680Q130,680 165,715Q200,750 200,800L80,800ZM280,800Q280,717 221.5,658.5Q163,600 80,600L80,520Q197,520 278.5,601.5Q360,683 360,800L280,800ZM440,800Q440,725 411.5,659.5Q383,594 334.5,545.5Q286,497 220.5,468.5Q155,440 80,440L80,360Q171,360 251,394.5Q331,429 391,489Q451,549 485.5,629Q520,709 520,800L440,800Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M400,656L240,496L296,440L400,544L664,280L720,336L400,656Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,920L480,840L760,840Q760,840 760,840Q760,840 760,840L760,800L600,800L600,480L760,480L760,440Q760,324 678,242Q596,160 480,160Q364,160 282,242Q200,324 200,440L200,480L360,480L360,800L200,800Q167,800 143.5,776.5Q120,753 120,720L120,440Q120,366 148.5,300.5Q177,235 226,186Q275,137 340.5,108.5Q406,80 480,80Q554,80 619.5,108.5Q685,137 734,186Q783,235 811.5,300.5Q840,366 840,440L840,840Q840,873 816.5,896.5Q793,920 760,920L480,920ZM200,720L280,720L280,560L200,560L200,720Q200,720 200,720Q200,720 200,720ZM680,720L760,720L760,560L680,560L680,720ZM200,560Q200,560 200,560Q200,560 200,560L200,560L280,560L280,560L200,560ZM680,560L680,560L760,560L760,560L680,560Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M710,598L652,540Q666,517 673,492Q680,467 680,440L760,440Q760,484 747,523.5Q734,563 710,598ZM480,366Q480,366 480,366L480,366L480,366L480,366Q480,366 480,366Q480,366 480,366Q480,366 480,366ZM592,478L520,406L520,200Q520,183 508.5,171.5Q497,160 480,160Q463,160 451.5,171.5Q440,183 440,200L440,326L360,246L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L600,440Q600,451 597.5,460Q595,469 592,478ZM440,840L440,717Q336,703 268,624Q200,545 200,440L280,440Q280,523 337.5,581.5Q395,640 480,640Q514,640 544.5,629.5Q575,619 600,600L657,657Q628,680 593.5,696Q559,712 520,717L520,840L440,840ZM792,904L56,168L112,112L848,848L792,904Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M395,525Q360,490 360,440L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L600,440Q600,490 565,525Q530,560 480,560Q430,560 395,525ZM480,320Q480,320 480,320L480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320L480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM440,840L440,717Q336,703 268,624Q200,545 200,440L280,440Q280,523 338.5,581.5Q397,640 480,640Q563,640 621.5,581.5Q680,523 680,440L760,440Q760,545 692,624Q624,703 520,717L520,840L440,840ZM508.5,468.5Q520,457 520,440L520,200Q520,183 508.5,171.5Q497,160 480,160Q463,160 451.5,171.5Q440,183 440,200L440,440Q440,457 451.5,468.5Q463,480 480,480Q497,480 508.5,468.5Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480Q414,480 367,433ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400Q513,400 536.5,376.5ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M760,480Q760,363 678.5,281.5Q597,200 480,200L480,120Q555,120 620.5,148.5Q686,177 734.5,225.5Q783,274 811.5,339.5Q840,405 840,480L760,480ZM600,480Q600,430 565,395Q530,360 480,360L480,280Q563,280 621.5,338.5Q680,397 680,480L600,480ZM798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM241,360L307,294Q307,294 307,294Q307,294 307,294L290,200Q290,200 290,200Q290,200 290,200L201,200Q201,200 201,200Q201,200 201,200Q206,241 215,281Q224,321 241,360ZM599,718Q638,735 678.5,745Q719,755 760,758Q760,758 760,758Q760,758 760,758L760,670Q760,670 760,670Q760,670 760,670L666,651Q666,651 666,651Q666,651 666,651L599,718ZM241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360ZM599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M320,680Q287,680 263.5,656.5Q240,633 240,600L240,360Q240,327 263.5,303.5Q287,280 320,280L360,280L400,240L560,240L600,280L640,280Q673,280 696.5,303.5Q720,327 720,360L720,600Q720,633 696.5,656.5Q673,680 640,680L320,680ZM320,600L640,600Q640,600 640,600Q640,600 640,600L640,360Q640,360 640,360Q640,360 640,360L320,360Q320,360 320,360Q320,360 320,360L320,600Q320,600 320,600Q320,600 320,600ZM480,560Q513,560 536.5,536.5Q560,513 560,480Q560,447 536.5,423.5Q513,400 480,400Q447,400 423.5,423.5Q400,447 400,480Q400,513 423.5,536.5Q447,560 480,560ZM342,20Q376,9 410.5,4.5Q445,0 480,0Q574,0 657.5,33.5Q741,67 805.5,126.5Q870,186 911,266.5Q952,347 960,440L880,440Q873,368 842,305.5Q811,243 762.5,195.5Q714,148 651,118Q588,88 516,82L578,144L522,200L342,20ZM618,940Q584,951 549.5,955.5Q515,960 480,960Q386,960 302.5,926.5Q219,893 154.5,833.5Q90,774 49,693.5Q8,613 0,520L80,520Q88,592 118.5,654.5Q149,717 197.5,764.5Q246,812 309,842Q372,872 444,878L382,816L438,760L618,940ZM480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M880,700L720,540L720,607L640,527L640,240Q640,240 640,240Q640,240 640,240L353,240L273,160L640,160Q673,160 696.5,183.5Q720,207 720,240L720,420L880,260L880,700ZM822,934L26,138L82,82L878,878L822,934ZM498,385L498,385Q498,385 498,385Q498,385 498,385L498,385ZM382,496Q382,496 382,496Q382,496 382,496L382,496Q382,496 382,496Q382,496 382,496L382,496Q382,496 382,496Q382,496 382,496ZM160,160L240,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L640,720Q640,720 640,720Q640,720 640,720L640,640L720,720L720,720Q720,753 696.5,776.5Q673,800 640,800L160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L640,160Q673,160 696.5,183.5Q720,207 720,240L720,420L880,260L880,700L720,540L720,720Q720,753 696.5,776.5Q673,800 640,800L160,800ZM160,720L640,720Q640,720 640,720Q640,720 640,720L640,240Q640,240 640,240Q640,240 640,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M560,829L560,747Q650,721 705,647Q760,573 760,479Q760,385 705,311Q650,237 560,211L560,129Q684,157 762,254.5Q840,352 840,479Q840,606 762,703.5Q684,801 560,829ZM120,600L120,360L280,360L480,160L480,800L280,600L120,600ZM560,640L560,318Q607,340 633.5,384Q660,428 660,480Q660,531 633.5,574.5Q607,618 560,640ZM400,354L314,440L200,440L200,520L314,520L400,606L400,354ZM300,480L300,480L300,480L300,480L300,480L300,480Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,343 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/call_root_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
android:fitsSystemWindows="false">
|
||||
|
||||
<!-- Remote Video (Full Screen, Black background) -->
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/remote_video_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/remote_avatar_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="Contact photo"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:layout_constraintWidth_percent="0.4"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:visibility="visible"
|
||||
tools:src="@drawable/ic_person" />
|
||||
|
||||
<!-- Top Bar -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="@color/calls_dark_surface"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/display_name_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/calls_dark_on_surface"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="John Doe" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/calls_dark_on_surface_variant"
|
||||
android:textSize="14sp"
|
||||
tools:text="Connecting..." />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- Local Video (Top Right) -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/local_video_container"
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_margin="16dp"
|
||||
app:cardElevation="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/local_video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Caller Icon (during incoming call prompt) -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/caller_icon_container"
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="140dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="70dp"
|
||||
app:cardElevation="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar"
|
||||
app:layout_constraintBottom_toTopOf="@id/incoming_call_prompt"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="0.35"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/caller_icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="Caller photo"
|
||||
tools:src="@drawable/ic_person" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Answer Mode Selector -->
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/answer_mode_selector"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/caller_icon_container"
|
||||
app:layout_constraintBottom_toTopOf="@id/incoming_call_prompt"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<Button
|
||||
android:id="@+id/answer_audio_only_button"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Audio"
|
||||
android:textSize="14sp"
|
||||
android:minWidth="100dp"
|
||||
app:icon="@drawable/ic_call"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="20dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/answer_video_button"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Video"
|
||||
android:textSize="14sp"
|
||||
android:minWidth="100dp"
|
||||
app:icon="@drawable/ic_videocam_on"
|
||||
app:iconGravity="textStart"
|
||||
app:iconSize="20dp" />
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<!-- Incoming Call Buttons -->
|
||||
<LinearLayout
|
||||
android:id="@+id/incoming_call_prompt"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:padding="24dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/decline_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="Decline"
|
||||
android:textSize="15sp"
|
||||
app:icon="@drawable/ic_call_end"
|
||||
app:iconGravity="textStart"
|
||||
app:cornerRadius="28dp"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
app:backgroundTint="@color/calls_red" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/answer_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Answer"
|
||||
android:textSize="15sp"
|
||||
app:icon="@drawable/ic_call"
|
||||
app:iconGravity="textStart"
|
||||
app:cornerRadius="28dp"
|
||||
style="@style/Widget.Material3.Button"
|
||||
app:backgroundTint="@color/calls_green" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Call Controls -->
|
||||
<LinearLayout
|
||||
android:id="@+id/call_controls_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_max="600dp">
|
||||
|
||||
<!-- Mute Button -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/mute_button"
|
||||
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"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Video Button -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/video_button"
|
||||
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"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- End Call Button -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/end_call_button"
|
||||
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"
|
||||
app:backgroundTint="@color/calls_red"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Speaker Button -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/speaker_button"
|
||||
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"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Switch Camera Button -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/switch_camera_button"
|
||||
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"
|
||||
app:fabSize="normal"
|
||||
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
|
||||
app:borderWidth="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -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, proxy_settings" />
|
||||
outbox, smtp_login, smtp_password, smtp_server, smtp_port, smtp_security_label, smtp_security, cert_check_label, cert_check, view_log_button" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/advanced_icon"
|
||||
@@ -157,17 +157,6 @@
|
||||
app:layout_constraintStart_toEndOf="@id/advanced_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/no_servers_hint" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/proxy_settings"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/proxy_use_proxy"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/advanced_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/inbox"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -176,7 +165,7 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/login_inbox"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/proxy_settings" />
|
||||
app:layout_constraintTop_toBottomOf="@id/advanced_text" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/imap_login"
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/status_bar_background" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/relay_list"
|
||||
<ScrollView
|
||||
android:id="@+id/relay_scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="88dp"
|
||||
android:scrollbars="vertical"
|
||||
tools:listitem="@layout/relay_list_item" />
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/relay_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:scrollbars="none"
|
||||
tools:listitem="@layout/relay_list_item" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transport_list_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="20dp"
|
||||
android:text="@string/transport_list_hint"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
|
||||
android:id="@+id/fab_add_relay"
|
||||
@@ -28,4 +52,4 @@
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/add_transport" />
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Audio Output"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/audio_device_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/device_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="@drawable/ic_volume_up" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/device_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="16sp"
|
||||
tools:text="Speaker" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/device_check"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_check"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/selected" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -157,8 +157,6 @@
|
||||
<string name="import_backup_title">استيراد نسخة احتياطية</string>
|
||||
<string name="delete_account">حذف الحساب</string>
|
||||
<!-- preferences -->
|
||||
<string name="pref_using_custom">استخدام التخصيص: %s</string>
|
||||
<string name="pref_using_default">مستخدم كافتراضي: %s</string>
|
||||
<string name="pref_profile_info_headline">معلومات ملفك التعريفي</string>
|
||||
<string name="pref_profile_photo">صورة الملف التعريفي</string>
|
||||
<string name="pref_blocked_contacts">المراسلون المحبوسون</string>
|
||||
@@ -174,8 +172,6 @@
|
||||
<string name="pref_led_color">لون ضوء التنبيه LED</string>
|
||||
<string name="pref_sound">الصوت</string>
|
||||
<string name="pref_privacy">الخصوصية</string>
|
||||
<!-- deprecated, use pref_chats -->
|
||||
<string name="pref_chats_and_media">الدردشة والوسائط</string>
|
||||
<string name="pref_system_default">افتراضيات النظام</string>
|
||||
<!-- Translators: as in "opposite of dark" -->
|
||||
<string name="pref_light_theme">فاتح</string>
|
||||
@@ -190,7 +186,7 @@
|
||||
<string name="pref_background">الخلفية</string>
|
||||
<string name="pref_show_emails_all">كلها</string>
|
||||
<string name="qrscan_fingerprint_label">البصمة</string>
|
||||
<!-- notifications -->
|
||||
<!-- notifications -->
|
||||
<string name="notify_reply_button">رد</string>
|
||||
<string name="notify_new_message">رسالة جديدة</string>
|
||||
<string name="notify_priority_high">عالي</string>
|
||||
|
||||
@@ -183,7 +183,6 @@
|
||||
<string name="contacts_title">Kontaktlar</string>
|
||||
<string name="contacts_type_email_above">E-poçt ünvanını yazın</string>
|
||||
|
||||
|
||||
<!-- chatlist and chat view -->
|
||||
<plurals name="chat_archived">
|
||||
<item quantity="one">%d çat arxiv olundu</item>
|
||||
@@ -215,12 +214,6 @@
|
||||
<string name="save_message">Saxla</string>
|
||||
<string name="saved_messages">Saxlanılmış mesajlar</string>
|
||||
<string name="retry_send">Mesaji yenidən göndərməyə cəht et</string>
|
||||
<!-- map -->
|
||||
<string name="filter_map_on_time">Vaxt məhdudiyyəti ilə yerləşmə yerləri göstərin</string>
|
||||
<string name="show_location_traces">İzləri göstərmək</string>
|
||||
<string name="add_poi">Maraq nöqtəsini göndərin</string>
|
||||
|
||||
|
||||
<!-- search -->
|
||||
<string name="search">Axtarış</string>
|
||||
<string name="search_explain">Çatlar, kontaktlar və mesajlar axtar</string>
|
||||
@@ -269,10 +262,7 @@
|
||||
<string name="forward_to">İrəli ...</string>
|
||||
<string name="share_abort">İcazə olmadığına görə göndərmə kəsildi</string>
|
||||
|
||||
|
||||
<!-- preferences -->
|
||||
<string name="pref_using_custom">:Xüsusi istifadə %s</string>
|
||||
<string name="pref_using_default">Varsayılan istifadə: %s</string>
|
||||
<string name="pref_profile_info_headline">Profil məlumatınız</string>
|
||||
<string name="pref_profile_photo">Profil şəkili</string>
|
||||
<string name="pref_blocked_contacts">Bloklanmış kontaktlar</string>
|
||||
@@ -298,8 +288,6 @@
|
||||
<string name="pref_sound">Səs</string>
|
||||
<string name="pref_silent">Səssiz</string>
|
||||
<string name="pref_privacy">Gizlilik</string>
|
||||
<!-- deprecated, use pref_chats -->
|
||||
<string name="pref_chats_and_media">Çatlar və faillar</string>
|
||||
<!-- Translators: as in "opposite of dark" -->
|
||||
<string name="pref_light_theme">Açıq</string>
|
||||
<string name="pref_dark_theme">Tünd</string>
|
||||
@@ -376,7 +364,7 @@
|
||||
<string name="qrshow_join_contact_hint">%1$silə əlaqə qurmaq üçün bunu skan edin</string>
|
||||
<string name="qrshow_join_contact_no_connection_toast">İnternet bağlantısı yoxdur, QR kodunu qura bilmir.</string>
|
||||
<string name="contact_verified">%1$s yoxlanıldı.</string>
|
||||
<!-- notifications -->
|
||||
<!-- notifications -->
|
||||
<string name="notify_reply_button">Cavab ver</string>
|
||||
<string name="notify_new_message">Yeni mesaj</string>
|
||||
<string name="notify_background_connection_enabled">Arxa fon əlaqəsi aktivdir</string>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<string name="off">Изключено</string>
|
||||
<string name="def">По подразбиране</string>
|
||||
<string name="default_value">По подразбиране (%1$s)</string>
|
||||
<string name="default_value_as_above">По подразбиране (същото като по-горе)</string>
|
||||
<string name="custom">Дефиниран от потребителя</string>
|
||||
<string name="none">Без</string>
|
||||
<string name="automatic">Автоматично</string>
|
||||
@@ -96,7 +95,7 @@
|
||||
<string name="last_month">Миналия месец</string>
|
||||
<!-- Refers to the time a contact was last seen. Shown below contact name in the profile. The placeholder will be replaced by date or time, resulting in "Last seen at 12:13 AM" or "Last seen Nov 12" -->
|
||||
<string name="last_seen_at">Последно видян на %1$s</string>
|
||||
<!-- Refers to the time a contact was last seen. Shown below contact name in the profile. The placeholder will be replaced by a relative point in time as "3 minutes ago" (see https://momentjs.com for more examples and languages)-->
|
||||
<!-- Refers to the time a contact was last seen. Shown below contact name in the profile. The placeholder will be replaced by a relative point in time as "3 minutes ago" (see https://momentjs.com for more examples and languages) -->
|
||||
<string name="last_seen_relative">Последно видян %1$s</string>
|
||||
<string name="last_seen_unknown">Последно видян: Не е известно</string>
|
||||
<!-- Shown beside messages that are "N minutes old". Prefer short strings, or well-known abbreviations. -->
|
||||
@@ -200,8 +199,6 @@
|
||||
<string name="menu_new_chat">Нов чат</string>
|
||||
<string name="menu_new_group">Нова група</string>
|
||||
<string name="clone_chat">Клониране на чат</string>
|
||||
<!-- deprecated -->
|
||||
<string name="add_recipients">Добавяне на получатели</string>
|
||||
<string name="menu_send">Изпращане</string>
|
||||
<string name="menu_toggle_keyboard">Включване/изключване на Emoji клавиатура</string>
|
||||
<string name="menu_edit_group">Редактиране на група</string>
|
||||
@@ -262,13 +259,12 @@
|
||||
<string name="menu_more_options">Още опции</string>
|
||||
<string name="menu_learn_spelling">Запомняне на правописа</string>
|
||||
<string name="jump_to_message">Към съобщение</string>
|
||||
<string name="copy_json">Копиране на JSON</string>
|
||||
<string name="replace_draft">Замяна на чернова</string>
|
||||
<string name="title_share_location">Местоположението да се споделя с всички членове на групата</string>
|
||||
<string name="device_talk">Съобщения в устройството</string>
|
||||
<string name="device_talk_subtitle">Локално генерирани съобщения</string>
|
||||
<string name="device_talk_explain">Съобщенията в този чат се генерират на Вашето устройство, за да информират относно актуализации на приложението и проблеми по време на ползване.</string>
|
||||
<string name="device_talk_welcome_message2">Свържете се!\n\n🙌 Натиснете \"QR код\" в главния екран и на двете устройства. Изберете \"Сканиране на QR код\" на едно от устройствата и го насочете към другото\n\n🌍 Ако двете устройства не се намират в една и съща стая, сканирайте посредством видео обаждане или споделете връзка с покана от \"Сканиране на QR код\"\n\nСлед това: Насладете се на изживяването да обменяте съобщения през най-голямата децентрализирана мрежа, която някога е съществувала: електронната поща. And in contrast to other popular apps, without central control or tracking or selling you, friends, colleagues or family out to large organizations.</string>
|
||||
<string name="device_talk_welcome_message2">Свържете се!\n\n🙌 Натиснете \"QR код\" в главния екран и на двете устройства. Изберете \"Сканиране на QR код\" на едно от устройствата и го насочете към другото\n\n🌍 Ако двете устройства не се намират в една и съща стая, сканирайте посредством видео обаждане или споделете връзка с покана от \"Сканиране на QR код\"\n\nСлед това: Насладете се на изживяването да обменяте съобщения през най-голямата децентрализирана мрежа, която някога е съществувала: електронната поща. And in contrast to other popular apps, without central control or tracking or selling you, friends, colleagues or family out to large organizations.</string>
|
||||
<string name="edit_contact">Редактиране на контакт</string>
|
||||
<!-- Verb "to pin", making something sticky, not a noun or abbreviation for "pin number". -->
|
||||
<string name="pin_chat">Забождане на чата</string>
|
||||
@@ -314,12 +310,10 @@
|
||||
<string name="ask_remove_members">Премахване на %1$s от групата?</string>
|
||||
<string name="open_url_confirmation">Искате ли да отворите тази връзка?</string>
|
||||
|
||||
|
||||
<!-- contact list -->
|
||||
<string name="contacts_title">Контакти</string>
|
||||
<string name="contacts_type_email_above">Въведете адрес за електронна поща по-горе</string>
|
||||
|
||||
|
||||
<!-- chatlist and chat view -->
|
||||
<plurals name="chat_archived">
|
||||
<item quantity="one">%d чат е архивиран</item>
|
||||
@@ -376,22 +370,14 @@
|
||||
<string name="send_file_to">Изпращане на \"%1$s\" до</string>
|
||||
<!-- title shown above a list contacts where one should be selected (eg. when a webxdc attempts to send a message to a chat) -->
|
||||
<string name="send_message_to">Изпращане на съобщение до…</string>
|
||||
<!-- map -->
|
||||
<string name="filter_map_on_time">Показване на местоположенията във времева рамка</string>
|
||||
<string name="show_location_traces">Показване на съобщенията за проследяване</string>
|
||||
<string name="add_poi">Изпращане на място, представляващо интерес</string>
|
||||
|
||||
|
||||
<!-- punycode warning / labeled links -->
|
||||
<!-- placeholder is domain/hostname that should be trusted -->
|
||||
<string name="open_external_url_trust_domain">Да не се пита повече за %1$s</string>
|
||||
<string name="puny_code_warning_header">Открита е подозрителна връзка</string>
|
||||
<!-- placeholder contains the hostname converted to ascii -->
|
||||
<string name="puny_code_warning_question">Сигурни ли сте, че искате да посетите %1$s?</string>
|
||||
<!-- this message is shown whenever a link with non-latin characters is clicked. first placeholder is original hostname with special chars, second placeholder is hostname encoded in ascii -->
|
||||
<string name="puny_code_warning_description">Има вероятност тази връзка лъжливо да представя някои знаци чрез подобно изглеждащи такива от различни от текущата азбуки. Следването на връзката с етикет %1$s ще доведе до %2$s, което е обичайно за знаци, които не са от Latin набора. Ако не сте очаквали такива знаци, тази връзка може да причини вреда</string>
|
||||
|
||||
|
||||
<!-- search -->
|
||||
<string name="search">Търсене</string>
|
||||
<string name="search_in_chat">Търсене в чата</string>
|
||||
@@ -457,8 +443,6 @@
|
||||
<string name="incoming_messages">Входящи съобщения</string>
|
||||
<!-- Headline for the "Outbox" eg. in the "Connectivity" view -->
|
||||
<string name="outgoing_messages">Изходящи съобщения</string>
|
||||
<!-- deprecated -->
|
||||
<string name="storage_on_domain">Съхранение върху %1$s</string>
|
||||
<string name="connectivity">Свързаност</string>
|
||||
<!-- Shown in the title bar if the app is "Not connected"; prefer short strings. -->
|
||||
<string name="connectivity_not_connected">Не сте свързани</string>
|
||||
@@ -476,11 +460,10 @@
|
||||
<!-- Used for describing resource usage, resulting string will be eg. "1.2 GiB of 3 GiB used" -->
|
||||
<string name="part_of_total_used">Използвани са %1$s от %2$s</string>
|
||||
|
||||
|
||||
<!-- welcome and login -->
|
||||
<!-- Primary button on the welcome screen, allows to create an instant profile -->
|
||||
<string name="onboarding_create_instant_account">Създаване на нов профил</string>
|
||||
<!-- Secondary button on the welcome screen, allows to "Add as Second Device", "Restore from Backup" -->
|
||||
<!-- Secondary button on the welcome screen, allows to "Add as Second Device", "Restore from Backup" -->
|
||||
<string name="onboarding_alternative_logins">Вече имам профил</string>
|
||||
<!-- This is a button and a title, allowing to use existing, classic email, setting ports, passwords and so on -->
|
||||
<string name="manual_account_setup_option">Вход в класическа електронна поща</string>
|
||||
@@ -549,10 +532,7 @@
|
||||
<string name="share_text_multiple_chats">Да бъде ли изпратен този текст към %1$d чата?\n\n\"%2$s\"</string>
|
||||
<string name="share_abort">Споделянето е прекъснато поради липсващи позволения.</string>
|
||||
|
||||
|
||||
<!-- preferences -->
|
||||
<string name="pref_using_custom">Използва се дефинирано от Вас: %s</string>
|
||||
<string name="pref_using_default">Използва се подразбиращо се: %s</string>
|
||||
<string name="pref_profile_info_headline">Информация от Вашия профил</string>
|
||||
<string name="pref_profile_photo">Изображение на профила</string>
|
||||
<string name="pref_blocked_contacts">Блокирани контакти</string>
|
||||
@@ -582,8 +562,6 @@
|
||||
<string name="pref_sound">Звук</string>
|
||||
<string name="pref_silent">Без звук</string>
|
||||
<string name="pref_privacy">Поверителност</string>
|
||||
<!-- deprecated, use pref_chats -->
|
||||
<string name="pref_chats_and_media">Чатове и медийни файлове</string>
|
||||
<string name="pref_system_default">Подразбиращото се за системата</string>
|
||||
<!-- Translators: as in "opposite of dark" -->
|
||||
<string name="pref_light_theme">Светъл</string>
|
||||
@@ -637,17 +615,11 @@
|
||||
<string name="up_to_x_most_worse_quality_images">До %1$s, повечето от които изображения с по-лошо качество</string>
|
||||
<string name="up_to_x_most_balanced_quality_images">До %1$s, повечето от които изображения с балансирано качество</string>
|
||||
<string name="download_failed">Неуспешно сваляне</string>
|
||||
<!-- %1$s will be replaced by a human-readable number of bytes, eg. 32 KiB, 1 MiB. Resulting string eg. "1 MiB message" -->
|
||||
<string name="n_bytes_message">%1$s съобщение</string>
|
||||
<!-- %1$s will be replaced by human-readable date and time -->
|
||||
<string name="download_max_available_until">Сваляне на максимално наличното до %1$s</string>
|
||||
<string name="profile_image_select">Избор на изображение за профила</string>
|
||||
<string name="select_your_new_profile_image">Изберете новото изображение за профила си</string>
|
||||
<string name="profile_image_delete">Изтриване на изображението на профила</string>
|
||||
<string name="pref_show_tray_icon">Показване на иконата в System Tray</string>
|
||||
<string name="pref_edit_profile">Редактиране на профил</string>
|
||||
<string name="disable_imap_idle">Деактивиране на IMAP IDLE</string>
|
||||
<string name="disable_imap_idle_explain">Да не се използва разширението IMAP IDLE дори ако сървърът го поддържа. Ако тази опция бъде разрешена, извличането на съобщенията ще бъде забавено. Разрешете я само за тестване.</string>
|
||||
<string name="send_stats_to_devs">Да се изпраща статистика към разработчиците на Delta Chat</string>
|
||||
<!-- Emoji picker and categories -->
|
||||
<string name="emoji_search_results">Резултати от търсенето</string>
|
||||
@@ -726,10 +698,6 @@
|
||||
<string name="ephemeral_timer_seconds_by_you">Установихте таймера на изчезващите съобщения на %1$s</string>
|
||||
<!-- %1$s will be replaced by the number of seconds (always >1) the timer is set to, %2$s will be replaced by name of the contact -->
|
||||
<string name="ephemeral_timer_seconds_by_other">Таймерът на изчезващите съобщение е установен на %1$sсек. от %2$s.</string>
|
||||
<!-- deprecated, usage needs to be removed in core -->
|
||||
<string name="ephemeral_timer_1_minute_by_you">Установихте таймера на изчезващите съобщения на 1 минута.</string>
|
||||
<!-- deprecated, usage needs to be removed in core -->
|
||||
<string name="ephemeral_timer_1_minute_by_other">Таймерът на изчезващите съобщения е установен на 1 минута от %1$s.</string>
|
||||
<string name="ephemeral_timer_1_hour_by_you">Установихте таймера на изчезващите съобщения на 1 час.</string>
|
||||
<!-- %1$s will be replaced by name of the contact -->
|
||||
<string name="ephemeral_timer_1_hour_by_other">Таймерът на изчезващите съобщения е установен на 1 час от %1$s.</string>
|
||||
@@ -801,7 +769,7 @@
|
||||
<string name="qrshow_join_contact_hint">Сканирайте, за да чатите с %1$s</string>
|
||||
<string name="qrshow_join_contact_no_connection_toast">Няма връзка към Интернет, не може да бъде извършена настройка чрез QR код.</string>
|
||||
<string name="qraccount_ask_create_and_login">Да бъде ли създаден нов адрес за електронна поща на \"%1$s\" и да бъде ли осъществено влизане там?</string>
|
||||
<string name="set_name_and_avatar_explain">Задайте име, което Вашите контакти ще разпознават. Можете също да установите изображение за профил.</string>
|
||||
<string name="set_name_and_avatar_explain">Задайте име, което Вашите контакти ще разпознават. Можете също да установите изображение за профил.</string>
|
||||
<string name="please_enter_name">Моля, въведете име.</string>
|
||||
<string name="qraccount_qr_code_cannot_be_used">Не може да бъде създаден нов акаунт със сканирания QR код.</string>
|
||||
<!-- the placeholder will be replaced by the address of the profile -->
|
||||
@@ -815,14 +783,12 @@
|
||||
<!-- Shown in contact profile. The placeholder will be replaced by the name of the contact that introduced the contact. -->
|
||||
<string name="verified_by">Проверен от%1$s</string>
|
||||
<string name="verified_by_you">Представен от мен</string>
|
||||
<!-- deprecated -->
|
||||
<string name="verified_contact_required_explain">За да се гарантира криптирането от край до край, можете да добавяте в тази група само контакти, отбелязани със зелена отметка.\n\nБихте могли да се срещнете с лицата, с които желаете да контактувате, лично и да сканирате техните QR кодове, за да ги въведете.</string>
|
||||
<string name="mailto_dialog_header_select_chat">Изберете чат, към който да бъде пратено съобщението</string>
|
||||
<!-- first placeholder is the name of the chat -->
|
||||
<string name="confirm_replace_draft">%1$s вече има съобщение чернова, искате ли да го замените?</string>
|
||||
<string name="mailto_link_could_not_be_decoded">Неуспешно декодиране на mailto връзка: %1$s</string>
|
||||
|
||||
<!-- notifications -->
|
||||
<!-- notifications -->
|
||||
<string name="notify_reply_button">Отговор</string>
|
||||
<string name="notify_new_message">Ново съобщение</string>
|
||||
<string name="notify_background_connection_enabled">Връзката във фонов режим е разрешена</string>
|
||||
@@ -837,7 +803,6 @@
|
||||
<string name="new_messages_body">Имате нови съобщения</string>
|
||||
<string name="n_messages_in_m_chats">%1$d съобщения в %2$d чата</string>
|
||||
|
||||
|
||||
<!-- permissions -->
|
||||
<string name="perm_required_title">Изисква се позволение</string>
|
||||
<string name="perm_continue">Продължаване</string>
|
||||
@@ -858,9 +823,6 @@
|
||||
<string name="ImageEditorHud_flip">Обръщане</string>
|
||||
<string name="ImageEditorHud_rotate">Завъртане</string>
|
||||
|
||||
<!-- strings introduced on desktop. we want to share strings between the os, in general, please do not add generic strings here -->
|
||||
<string name="about_offical_app_desktop">Това е официалното настолно приложение Delta Chat.</string>
|
||||
<string name="about_licensed_under_desktop">Този софтуер е лицензиран под GNU GPL версия 3. Изходният код е наличен в GitHub.</string>
|
||||
<string name="welcome_desktop">Добре дошли в Delta Chat</string>
|
||||
<string name="global_menu_preferences_language_desktop">Език</string>
|
||||
<string name="global_menu_file_desktop">Файл</string>
|
||||
@@ -896,6 +858,7 @@
|
||||
<string name="menu.view.developer.open.log.folder">Отваряне на папката \"Дневник\"</string>
|
||||
<string name="menu.view.developer.open.current.log.file">Отваряне на текущия файл на дневника</string>
|
||||
<string name="explain_desktop_minimized_disabled_tray_pref" tools:ignore="TypographyDashes">Показването на икона в системната лента не може да бъде забранено, тъй като Delta Chat е стартиран с опцията --minimized.</string>
|
||||
<!-- deprecated, used on desktop although there is not spell checker -->
|
||||
<string name="no_spellcheck_suggestions_found">Няма открити предложения за правопис.</string>
|
||||
<string name="show_window">Показване на прозореца</string>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user