Compare commits

..

135 Commits

Author SHA1 Message Date
adbenitez 923e2a43a4 fix WebxdcActivity: restore addMultiAccountObserver
accidentally reverted to addObserver while resolving merge conflict
2026-06-30 04:48:27 +02:00
adbenitez f29f94f138 Merge remote-tracking branch 'upstream/main' 2026-06-29 20:26:34 +02:00
adbenitez 3a35374a03 allow webxdc in "unselected account" mode 2026-06-29 18:30:23 +02:00
adb fe0793481c Merge pull request #4497 from deltachat/adb/handle-chat-and-webxdc-deletion
when a webxdc is deleted close WebxdcActivity
2026-06-29 18:20:30 +02:00
adbenitez 3c2a672799 Merge branch 'adb/handle-chat-and-webxdc-deletion' of https://github.com/deltachat/deltachat-android into adb/handle-chat-and-webxdc-deletion 2026-06-29 17:33:26 +02:00
adbenitez 2476d17394 be backward compatible with existing shortcuts 2026-06-29 17:30:11 +02:00
adb 5ae021bde3 Merge branch 'main' into adb/handle-chat-and-webxdc-deletion 2026-06-29 16:47:02 +02:00
adb 9f46c242fc Merge pull request #4504 from deltachat/adb/issue-4501
cancel search mode when back is pressed
2026-06-29 16:45:01 +02:00
adbenitez cf36254c1a add margin top to subject field 2026-06-29 00:07:10 +02:00
adb 0d94bf2006 Merge branch 'main' into adb/handle-chat-and-webxdc-deletion 2026-06-28 23:25:46 +02:00
adbenitez 4757150aae apply spotless 2026-06-28 23:21:23 +02:00
adbenitez 0d76ab930f update changelog 2026-06-28 23:16:46 +02:00
adbenitez 5515e2fe3c cancel search mode when back is pressed 2026-06-28 23:14:55 +02:00
adb 508f320b4c Merge pull request #165 from ArcaneChat/make-input-panel-round
enhance the InputPanel: make it pill-like with rounder corners
2026-06-28 19:35:54 +02:00
adbenitez 176e6c94b2 apply spotless 2026-06-28 19:34:25 +02:00
adbenitez deb650970c enhance the InputPanel: make it pill-like with rounder corners 2026-06-28 19:30:10 +02:00
adbenitez 36fc2261f9 improve welcome screen 2026-06-27 02:47:08 +02:00
B. Petersen f11c6baaf6 shorten shortcut for 'Mark as Read'
all of WhatsApp/Signal/Telegram use 'Read' (past tense) for the shortcut of 'Mark as Read',
we also give that as an example in the translators hint.

this works, as there is enough context,
eg. the chat is long-tapped or swiped.

beside consistency, more reasons to follow that path:

- LiquidGlass has larger buttons, it looks better to stay short here

- while 'Mark Read' is short enough, it results in translations being to long -
  the shorter source, differing more from 'Mark as Read',
  should encourage translators to stay shorter

- surely UI should and do work also with longer texts, but it looks better to be short and on point

the string is mainly used by iOS, for android it is used in the notification,
where we switch to the longer form, that is very similar to what we had before
(but i would be fine with both)
2026-06-26 22:11:45 +02:00
adbenitez 6add250258 Merge remote-tracking branch 'upstream/main' 2026-06-26 20:29:09 +02:00
adbenitez d15efbdb44 update changelog 2026-06-26 19:05:21 +02:00
adbenitez 2422f9110c when a webxdc is deleted close WebxdcActivity
also if a chat is deleted, close webxdc, and chat instead of showing
weird "ghost chat"
2026-06-26 18:47:12 +02:00
wchen342 b6162c2044 Fix video capture to query camera formats and enforce 16:9 aspect ratio for video calls (#4496) 2026-06-26 17:34:28 +02:00
adb 75245cfa02 Merge pull request #4495 from deltachat/adb/issue-4494
properly handle messages with ~overrideSenderName
2026-06-24 15:45:49 +02:00
adb 0d19f058fe Merge branch 'main' into adb/issue-4494 2026-06-24 14:59:33 +02:00
adb 911e695187 Merge pull request #4493 from deltachat/adb/issue-4492
properly hide all input&drafting elements on in-chat search
2026-06-24 14:59:03 +02:00
adbenitez b8c8969162 fix typo in variable name 2026-06-24 14:51:47 +02:00
adbenitez 0610520064 also synchronize the changes in initializeContactRequest() 2026-06-23 22:57:14 +02:00
adbenitez 5ae60af73f avoid redundant code block 2026-06-23 22:51:28 +02:00
adbenitez dd6f1015dc Merge branch 'adb/issue-4492' of https://github.com/deltachat/deltachat-android into adb/issue-4492 2026-06-23 22:45:19 +02:00
adbenitez 74ea10ebef properly avoid revealing hidden views while in search mode 2026-06-23 22:44:29 +02:00
adbenitez 877f991131 improve code and fix notifyWebxdc and notifyReaction 2026-06-23 21:33:30 +02:00
adb e556878b2f Merge branch 'main' into adb/issue-4494 2026-06-23 20:21:32 +02:00
adb 99a1fee994 Merge branch 'main' into adb/issue-4492 2026-06-23 20:21:12 +02:00
adb dfd323a89f Merge pull request #4491 from deltachat/adb/allow-select-multiple-files
allow to select multiple files
2026-06-23 20:18:45 +02:00
adbenitez 5a086ff022 avoid input_area wrapper 2026-06-23 19:21:15 +02:00
adbenitez bedaa2c287 tweak comment 2026-06-23 18:43:22 +02:00
adbenitez 50037d5232 fix code format 2026-06-23 18:42:05 +02:00
adbenitez 6756b04da9 properly handle messages with ~overrideSenderName 2026-06-23 18:30:21 +02:00
adbenitez b12ecf66c5 tweak CHANGELOG 2026-06-23 16:21:24 +02:00
adb d05403db02 Merge branch 'adb/allow-select-multiple-files' into adb/issue-4492 2026-06-23 16:20:36 +02:00
adb 2d8344a61f Merge branch 'main' into adb/allow-select-multiple-files 2026-06-23 16:19:47 +02:00
wchen342 44afda55e5 Add notification for missed calls (#4485) 2026-06-23 16:12:16 +02:00
adbenitez bfa6eef604 update CHANGELOG 2026-06-22 20:28:26 +02:00
adbenitez 6327b30350 properly hide all input&drafting elements on in-chat search 2026-06-22 20:02:16 +02:00
adbenitez f92cac4d55 update CHANGELOG 2026-06-22 18:57:49 +02:00
adbenitez 0204fca27d allow to select multiple files 2026-06-22 18:37:30 +02:00
adbenitez eed844469b apply spotless 2026-06-15 23:21:12 +02:00
adbenitez 59ceae46a0 Merge remote-tracking branch 'upstream/main' 2026-06-15 23:05:19 +02:00
adbenitez 693188dcff Merge remote-tracking branch 'upstream/main' 2026-06-15 23:04:36 +02:00
adb 025ff086c8 Merge pull request #4487 from deltachat/prep-2.53.0
prepare 2.53.0
2026-06-15 22:47:25 +02:00
adb a802bfcb61 Merge branch 'main' into prep-2.53.0 2026-06-15 22:46:19 +02:00
adbenitez 1f18bfe2dd prepare 2.53.0 2026-06-15 22:28:18 +02:00
adb dc1bc77925 Merge pull request #4486 from deltachat/update-core-and-stuff-2026-06-15
Update core to 2.53.0
2026-06-15 22:24:28 +02:00
adbenitez ac6383d79a update changelog 2026-06-15 22:10:55 +02:00
adbenitez 59fec8462c update translations 2026-06-15 22:03:27 +02:00
adbenitez d64baa294f update deltachat-core-rust to 'chore(release): prepare for 2.53.0' of 'v2.53.0' 2026-06-15 21:59:16 +02:00
adb e7e14af158 Merge pull request #4482 from deltachat/adb/issue-4481
don't create mailto unencrypted chat if force-encryption is enabled
2026-06-14 01:57:19 +02:00
adb e094e96669 Merge branch 'main' into adb/issue-4481 2026-06-14 01:54:47 +02:00
adb fa56e8ca0d Merge pull request #4483 from deltachat/adb/fix-ci-for-outside-pr
don't try to upload artifacts for PRs from external contributors
2026-06-12 23:15:56 +02:00
wch423 3d5abdcd79 Correct CHANGELOG.md 2026-06-12 16:21:38 +02:00
wchen342 92c4e41b59 Call maybeNetwork() on network unblock (#4476)
Limit maybeNetwork() calls when network changes

* Call maybeNetwork() on network unblock to allow immediate fetch during Doze (API 29+)
* Restrict maybeNetwork() in onAvailable() to API 24-28
2026-06-12 14:11:10 +00:00
adbenitez bbfd0c31da Merge remote-tracking branch 'upstream/main' 2026-06-11 20:19:42 +02:00
adb d5f627be50 Merge pull request #4461 from d2weber/notif_msg_style
feat: use message style notifications
2026-06-11 20:18:38 +02:00
adbenitez f0ca58d53f don't try to upload artifacts for PRs from external contributors 2026-06-11 20:00:35 +02:00
adb 56f5655693 Merge branch 'main' into notif_msg_style 2026-06-11 19:44:16 +02:00
wchen342 310a7f99c8 Remove media notification when audio playback ends naturally (#4472) 2026-06-11 19:25:17 +02:00
adb d0f7fca8c9 Merge branch 'main' into adb/issue-4481 2026-06-11 19:05:27 +02:00
B. Petersen 80de08f980 remove remainings of is_chatmail usage
instead, force_ncryption is checked now,
and this only for for option "New Chat -> New Email".

this also fixes the bug that, for classic relays,
by default "force encryption" is set but we still show "new email".

what's left for now is a check for is_chatmail whether fcm is sufficient or not.
2026-06-11 19:01:50 +02:00
adb 80a0ff0098 Merge pull request #4473 from deltachat/adb/ci-update-ndk-30
update NDK version in CI workflows
2026-06-11 18:53:22 +02:00
adbenitez 9c55de17a3 don't create mailto unencrypted chat if force-encryption is enabled 2026-06-11 18:49:08 +02:00
d2weber 8c1bc8e70e apply messaging style also if !isDisplayMessage() 2026-06-10 10:25:58 +02:00
adbenitez 4659bbbe51 allow to open multiple webxdc at the same time 2026-06-10 04:40:49 +02:00
d2weber f0760d6695 make notification icon nice and big in all cases 2026-06-09 20:47:26 +02:00
adbenitez 68ffedc1d1 Merge remote-tracking branch 'upstream/main' 2026-06-09 18:41:01 +02:00
adb 4bdfe8e390 Merge pull request #4475 from deltachat/adb/fix-changelog-09-06-2026
fix date for 2.51.0
2026-06-09 18:24:11 +02:00
adbenitez f3a396daae fix date for 2.51.0 2026-06-09 18:22:27 +02:00
adb 6d038e9d7f Merge branch 'main' into adb/ci-update-ndk-30 2026-06-09 18:12:23 +02:00
adb 5d62a439d2 Merge pull request #4474 from deltachat/prep-2.52.0
prepare 2.52.0
2026-06-09 18:10:35 +02:00
adbenitez 46be278bf5 update ndk version 2026-06-09 17:58:01 +02:00
adbenitez ef6f8958ea prepare 2.52.0 2026-06-09 17:53:13 +02:00
adb 78c73b3f8c Merge pull request #4470 from deltachat/update-core-and-stuff-09-06-2026
Update core to 2.52.0
2026-06-09 17:38:45 +02:00
biörn 18dc39d266 Update BUILDING.md
Co-authored-by: adb <adb@merlinux.eu>
2026-06-09 17:37:08 +02:00
biörn 41cd10a76f Update BUILDING.md
Co-authored-by: adb <adb@merlinux.eu>
2026-06-09 17:37:08 +02:00
B. Petersen 7ccae5d3be docs: fix links in BUILDING.md
the redirect from `master` to `main` does not or no longer work or deeplinks.
2026-06-09 17:37:08 +02:00
adbenitez adbfbb9e77 update changelog 2026-06-09 15:53:57 +02:00
adbenitez dda7f34599 update RPC 2026-06-09 15:53:45 +02:00
adbenitez bf5a5dc9e9 update translations 2026-06-09 15:53:38 +02:00
adbenitez 84faf4115b update deltachat-core-rust to 'chore(release): prepare for 2.52.0' of 'v2.52.0' 2026-06-09 15:48:47 +02:00
wchen342 1577b90047 Merge branch 'main' into notif_msg_style 2026-06-05 20:04:12 +02:00
wchen342 fc94684d04 Remove condition on upload preview apk workflow (#4463) 2026-06-05 20:03:58 +02:00
wchen342 b92ff80e3a Fix call not ending correctly under certain conditions (#4462) 2026-06-05 20:03:38 +02:00
d2weber 515a84b161 Merge branch 'main' into notif_msg_style 2026-06-04 20:04:12 +02:00
d2weber d757ef59c0 fixup: displayContact without displayMessage 2026-06-04 18:12:03 +02:00
wchen342 cabcc71545 Save audio draft on interrupt (#4460)
* Save audio draft on interrupt

* Fix pre-existing bugs while attaching audio files as draft

* Set audio attachment in draft properly
2026-06-04 18:02:20 +02:00
d2weber d0a584d15d fixup: cleanup getAvatar 2026-06-04 11:36:53 +02:00
d2weber f8ec6d2da2 fixup: reactions and webxdc updates 2026-06-04 11:09:24 +02:00
d2weber c3b7e06b86 add isBot to person (not sure if it has any effect) 2026-06-03 22:16:05 +02:00
d2weber d5edcea5d0 fixup: convert timestamp to ms 2026-06-03 22:07:32 +02:00
d2weber 8a3d29fca3 fixup: handle null from getAvatar() 2026-06-03 22:06:15 +02:00
d2weber 6156ee0534 feat: use message style notifications
With MessagingStyle, Notifications can be expanded to show more text of
the message than just a single line.

This feature has been requested in
https://support.delta.chat/t/swipe-down-on-notification-to-expand-it/2742
2026-06-03 19:53:21 +02:00
adb 1cad6801b6 Merge pull request #4458 from deltachat/adb/issue-4457
avoid NPE in MediaView.set()
2026-06-01 18:53:32 +02:00
adbenitez a73af53c50 apply spotless 2026-06-01 16:20:20 +02:00
adbenitez 8eacd09336 update changelog 2026-06-01 16:16:19 +02:00
adbenitez 673926b1ee avoid NPE in MediaView.set()
instead throw IOException as it is already expected for invalid media types
2026-06-01 16:13:52 +02:00
adb 643b7cc381 Merge pull request #4456 from d2weber/update-building-md
fix: update 'fat' to 'foss' in BUILDING.md
2026-06-01 16:08:49 +02:00
d2weber 42db29304a fix: missing dep in flake.nix 2026-05-31 15:35:34 +00:00
d2weber 323dbde715 fix: update fat->foss in BUILDING.md 2026-05-31 17:34:04 +02:00
adbenitez b9649a908e Merge remote-tracking branch 'upstream/main' 2026-05-30 17:18:28 +02:00
adb 2f20f558ad Merge pull request #4453 from deltachat/prep-2.51.0
prepare 2.51.0
2026-05-30 03:06:36 +02:00
adbenitez 6f868f653a prepare 2.51.0 2026-05-30 01:01:01 +02:00
adb 42b529dd4a Merge pull request #4452 from deltachat/update-core-and-stuff-30-05-2026
Update core to 2.51.0
2026-05-30 00:31:37 +02:00
adbenitez d7eab73687 update translations 2026-05-30 00:17:36 +02:00
adbenitez 1deebdf552 update deltachat-core-rust to 'chore(release): prepare for 2.51.0' of 'v2.51.0' 2026-05-30 00:14:45 +02:00
wchen342 20fbb0e1ee Reset scroll position after switching account (#4451) 2026-05-29 16:25:16 +02:00
adb 6a53e229d1 Merge pull request #4450 from deltachat/adb/fix-notifyCalls-toggle
fix notifyCalls toggle state setting
2026-05-29 15:11:49 +02:00
adbenitez 74ef2d478a fix notifyCalls toggle state setting 2026-05-29 15:07:05 +02:00
adbenitez feeece405d fix WebxdcActivity 2026-05-29 02:15:06 +02:00
adbenitez 20459a5710 Merge remote-tracking branch 'upstream/main' 2026-05-28 23:42:34 +02:00
adb a3a8b3581f Merge pull request #4449 from deltachat/adb/issue-4445
fix sharing contact across profiles
2026-05-28 23:35:38 +02:00
adbenitez f2571dba91 fix sharing contact across profiles 2026-05-28 20:04:27 +02:00
adb 708ffe617c Merge pull request #4448 from deltachat/adb/issue-4447
respect IMAP Folder field if set
2026-05-28 19:39:47 +02:00
adbenitez f6ecb94047 respect IMAP Folder field if set 2026-05-28 19:30:07 +02:00
link2xt 2d4b52b037 fixup 2026-05-27 16:35:13 +00:00
link2xt 5257f39aee Fix debouncing in chatlist and search
Reset inLoadChatlist only right before exit
in loadChatlistAsync().
Otherwise if loadChatlistAsync() is called
while the loop is running loadChatlist() or Util.sleep(100),
loadChatlistAsync() will see inLoadChatlist=false,
set needsAnotherLoad=true and start a second loop.
This way it is possible to spawn any number of background loops
running loadChatlist() simultaneously
so no debouncing actually happened.

Debouncing in SearchViewModel.updateQuery()
is fixed similarly by copying the code structure from loadChatlistAsync().
Previously it did not even have the lock.
2026-05-27 16:35:13 +00:00
adb 20f1475856 Merge pull request #4366 from deltachat/adb/add-isbroadcast-isappsender
add new webxdc.isAppSender and webxdc.isBroadcast APIs
2026-05-26 20:58:54 +02:00
adb db0295e5a6 Merge branch 'main' into adb/add-isbroadcast-isappsender 2026-05-26 20:55:38 +02:00
wchen342 d019c1bfb5 Use new RPC for updating and stopping location streaming (#4400) 2026-05-26 18:02:13 +02:00
adbenitez 4542ba1b9a update changelog 2026-05-26 17:29:06 +02:00
adb 9b982db899 Merge branch 'main' into adb/add-isbroadcast-isappsender 2026-05-26 17:12:03 +02:00
adbenitez a1a72b79b3 use Log.e instead of Log.i 2026-05-26 17:11:19 +02:00
adbenitez 319e2ebe5a use WebxdcMessageInfo everywhere 2026-05-25 18:15:32 +02:00
adbenitez 49a00f3788 apply spotless 2026-05-25 17:37:13 +02:00
adb 74ee2fb8db Merge branch 'main' into adb/add-isbroadcast-isappsender 2026-05-25 16:53:18 +02:00
adb 655ed8cad4 Merge branch 'main' into adb/add-isbroadcast-isappsender 2026-04-29 17:19:36 +02:00
adbenitez 1d94fe8bca add new webxdc.isAppSender and webxdc.isBroadcast APIs 2026-04-18 22:36:43 +02:00
82 changed files with 2545 additions and 1325 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
ndk-version: "r29"
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
+3 -2
View File
@@ -11,7 +11,6 @@ permissions: {}
jobs:
build:
name: Upload Preview APK
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
@@ -46,7 +45,7 @@ jobs:
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
ndk-version: "r29"
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
@@ -78,12 +77,14 @@ jobs:
- name: Upload APK
id: upload
if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: app-preview.apk
path: 'build/outputs/apk/foss/debug/*.apk'
- name: Add artifact links to PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
+5 -5
View File
@@ -47,7 +47,7 @@ Nix development environment contains Rust with cross-compilation toolchains and
To [build an APK](https://developer.android.com/studio/build/building-cmdline) run the following 2 steps.
Note that the first step may take some time to build for all architectures. You can optionally read
[the first comment block in the `ndk-make.sh` script](https://github.com/deltachat/deltachat-android/blob/master/scripts/ndk-make.sh)
[the first comment block in the `ndk-make.sh` script](./scripts/ndk-make.sh)
for pointers on how to build for a specific architecture.
```
$ scripts/ndk-make.sh
@@ -56,7 +56,7 @@ $ ./gradlew assembleDebug
Resulting APK files can be found in
`build/outputs/apk/gplay/debug/` and
`build/outputs/apk/fat/debug/`.
`build/outputs/apk/foss/debug/`.
## Build Using Dockerfile
@@ -114,7 +114,7 @@ deltachat@6012dcb974fe:/home/app$ ./gradlew assembleDebug
In /etc/containers/storage.conf, replace the line: `driver = ""` with: `driver = "overlay"`.
You can also set the `driver` option to something else, you just need to set it to _something_.
[Read about possible options here](https://github.com/containers/storage/blob/master/docs/containers-storage.conf.5.md#storage-table).
[Read about possible options here](https://github.com/containers/storage/blob/main/docs/containers-storage.conf.5.md#storage-table).
## <a name="setup-podman"></a>Setup Podman
@@ -135,8 +135,8 @@ See https://wiki.archlinux.org/index.php/Podman#Rootless_Podman for more informa
To setup build environment manually:
- _Either_, in Android Studio, go to "Tools / SDK Manager / SDK Tools", enable "Show Package Details",
select "CMake" and the desired NDK (install the same NDK version as the [Dockerfile](https://github.com/deltachat/deltachat-android/blob/master/Dockerfile)), hit "Apply".
- _Or_ read [Dockerfile](https://github.com/deltachat/deltachat-android/blob/master/Dockerfile) and mimic what it does.
select "CMake" and the desired NDK (install the same NDK version as the [Dockerfile](./Dockerfile)), hit "Apply".
- _Or_ read [Dockerfile](./Dockerfile) and mimic what it does.
Then, in both cases, install Rust using [rustup](https://rustup.rs/)
and Rust toolchains for cross-compilation by executing `scripts/install-toolchains.sh`.
+37 -3
View File
@@ -1,7 +1,36 @@
# Delta Chat Android Changelog
## v2.50.0
2026-05
## Unreleased
* Allow to select multiple files for sending
* Add notifications for missed calls
* Video call preview now accurately shows what is sent to remote
* Fix: properly hide draft attachment during in-chat search
* Fix: close mini-apps and chats if they are deleted
* Fix: cancel in-chat search when back is pressed, instead of directly returning to chatlist
## v2.53.0
2026-06
* Use message style notifications for longer message previews
* Remove notification after audio playback ends
* Fix: do not allow blocked contacts to use our invite links
* Fix sending mini-app that was used/prepared before sending
* Some more small fixes and updated translations
* Update to core 2.53.0
## v2.52.0
2026-06
* Fix: avoid crashes in Media preview sometimes
* Fix: Incorrect total time when attaching audio files as draft
* Fix: Audio files in draft showing total time from wrong file
* Fix: Update the channel title after joining if the QR code had an outdated title
* Voice recording will be automatically saved as draft when interrupted
* Update to core 2.52.0
## v2.51.0
2026-06
* Better incoming call system integration
* Calls are not experimental anymore and don't need to be manually enabled
@@ -34,11 +63,16 @@
* Resend the last 10 messages to new broadcast channel member
* Enable PQC (Post-Quantum Cryptography) support. We do not generate PQC keys yet, this step is needed for forward compatibility
* Improve avatar quality
* Add new webxdc.isAppSender and webxdc.isBroadcast APIs for mini-apps
* Fix: avoid invalid empty "~" notifications when some peer is streaming location
* Fix: Improve detection of stickers
* Fix text direction issues for RTL languages in "Show full message" view
* Fix: Reconnect when removing a relay
* Update to core 2.50.0
* Fix: Location streaming now works correctly with multiple accounts
* Fix debouncing in chatlist and search
* Fix sharing contact across profiles
* Fix: Reset scroll location after switching account
* Update to core 2.51.0
## v2.49.0
2026-04
+2 -2
View File
@@ -34,8 +34,8 @@ android {
useLibrary 'org.apache.http.legacy'
defaultConfig {
versionCode 30000743
versionName "2.50.0"
versionCode 30000746
versionName "2.53.0"
applicationId "chat.delta.lite"
multiDexEnabled true
+1
View File
@@ -35,6 +35,7 @@
buildInputs = [
android-sdk
pkgs.openjdk17
pkgs.perl
(pkgs.buildPackages.rust-bin.stable."${rust-version}".minimal.override {
targets = [
"armv7-linux-androideabi"
+5
View File
@@ -493,6 +493,11 @@
android:exported="false"
android:foregroundServiceType="camera|microphone|phoneCall" />
<receiver
android:name=".calls.CallActionReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".notifications.MarkReadReceiver"
android:enabled="true"
+110 -111
View File
@@ -29,21 +29,21 @@
<li><a href="#cuántos-miembros-pueden-participar-en-un-solo-grupo">¿Cuántos miembros pueden participar en un solo grupo?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<li><a href="#channels">Canales</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
<li><a href="#suscribirse-a-un-canal">Suscribirse a un canal</a></li>
<li><a href="#crear-un-canal">Crear un canal</a></li>
<li><a href="#cuántos-suscriptores-puede-tener-un-canal">¿Cuántos suscriptores puede tener un canal?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<li><a href="#calls">Llamadas</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
<li><a href="#establecer-una-llamada">Establecer una llamada</a></li>
<li><a href="#aceptar-o-rechazar-una-llamada">Aceptar o rechazar una llamada</a></li>
<li><a href="#durante-una-llamada">Durante una llamada</a></li>
</ul>
</li>
<li><a href="#llamadas-perdidas-y-notificaciones">Llamadas perdidas y notificaciones</a></li>
<li><a href="#webxdc">Aplicaciones en chats</a>
<ul>
<li><a href="#dónde-puedo-conseguir-aplicaciones-para-el-chat">¿Dónde puedo conseguir aplicaciones para el chat?</a></li>
@@ -68,14 +68,14 @@
<li><a href="#tienen-planeado-crear-un-cliente-web-de-delta-chat">¿Tienen planeado crear un cliente web de Delta Chat?</a></li>
</ul>
</li>
<li><a href="#advanced">Advanced</a>
<li><a href="#avanzado">Avanzado</a>
<ul>
<li><a href="#experiments">Experimental Features</a></li>
<li><a href="#relays">What are Relays?</a></li>
<li><a href="#experiments">Características experimentales</a></li>
<li><a href="#relays">¿Qué son los servidores?</a></li>
<li><a href="#puedo-utilizar-una-dirección-de-email-clásico-con-delta-chat">¿Puedo utilizar una dirección de email clásico con Delta Chat?</a></li>
<li><a href="#classic-email">How can I configure a chat profile with a classic email address as relay?</a></li>
<li><a href="#i-want-to-manage-my-own-server-for-delta-chat-what-do-you-recommend">I want to manage my own server for Delta Chat. What do you recommend?</a></li>
<li><a href="#statssending">What is “Send statistics to Delta Chats developers”?</a></li>
<li><a href="#classic-email">¿Cómo puedo configurar un perfil de chat con una dirección clásica de email como servidor?</a></li>
<li><a href="#quiero-gestionar-mi-propio-servidor-de-delta-chat-qué-me-recomiendan">Quiero gestionar mi propio servidor de Delta Chat. ¿Qué me recomiendan?</a></li>
<li><a href="#statssending">¿Qué es “Enviar estasticas a los desarrolladores de Delta Chat”?</a></li>
<li><a href="#estoy-interesado-en-los-detalles-técnicos-pueden-decirme-más">Estoy interesado en los detalles técnicos. ¿Pueden decirme más?</a></li>
</ul>
</li>
@@ -84,10 +84,10 @@
<li><a href="#qué-estándares-se-utilizan-para-el-cifrado-de-extremo-a-extremo">¿Qué estándares se utilizan para el cifrado de extremo a extremo?</a></li>
<li><a href="#whene2e">¿Cómo puedo saber si los mensajes están cifrados de extremo a extremo?</a></li>
<li><a href="#puedo-enviar-o-recibir-mensajes-sin-cifrado-de-extremo-a-extremo">¿Puedo enviar o recibir mensajes sin cifrado de extremo a extremo?</a></li>
<li><a href="#e2eeguarantee">What does the green checkmark in a contact profile mean?</a></li>
<li><a href="#e2eeguarantee">¿Qué significa la marca de verificación verde en el perfil de un contacto?</a></li>
<li><a href="#los-adjuntos-imágenes-archivos-audio-etc-están-cifrados-de-extremo-a-extremo">¿Los adjuntos (imágenes, archivos, audio, etc.) están cifrados de extremo a extremo?</a></li>
<li><a href="#openpgp-secure">¿Es OpenPGP seguro?</a></li>
<li><a href="#openpgp-alternatives">Did you consider using alternatives to OpenPGP for end-to-end-encryption?</a></li>
<li><a href="#openpgp-alternatives">¿Considerasteis usar alternativas a OpenPGP para la encriptación de extremo a extremo?</a></li>
<li><a href="#es-delta-chat-vulnerable-a-efail">¿Es Delta Chat vulnerable a EFAIL?</a></li>
<li><a href="#tls">Are messages marked with the mail icon exposed on the Internet?</a></li>
<li><a href="#message-metadata">¿Cómo Delta Chat protege los metadatos en los mensajes?</a></li>
@@ -224,11 +224,11 @@ esto aparecerá como <b style="border: 1px solid currentColor; padding: 0 3px; f
</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>Adjunta el primer contacto al chat del segundo utilizando<img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Botón de adjuntar → Contacto</strong>.
También puedes añadir un mensaje introductorio.</p>
<p>The second contact will receive a <strong>card</strong> then
and can tap it to start chatting with the first contact.</p>
<p>El segundo contacto recibirá una <strong>tarjeta</strong>
y podrá pulsarla para empezar a chatear con el primer contacto.</p>
<h3 id="multiple-accounts">
@@ -349,13 +349,13 @@ Sea por el <a href="#edit">remitente</a>, por la <a href="#delold">limpieza del
</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>En ocasiones podrás ver un <strong>punto verde</strong><img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />
al lado del avatar de un contacto.
Esto significa que ha sido <strong>visto recientemente por ti</strong> en los últimos 10 minutos,
por ejemplo, porque te han enviado un mensaje o han enviado una confirmación de lectura.</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>Así que esto no es un estado en línea en tiempo real
y los demás tampoco verán siempre que estás “en línea”.</p>
<h3 id="qué-significan-las-marcas-que-se-muestran-junto-a-los-mensajes-salientes">
@@ -424,8 +424,8 @@ who could have already replied, forwarded, saved, screenshotted or otherwise cop
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>
<p>En regiones con una mala conectividad,
puedes elegir una mayor compresión en <strong>Ajustes → Chats → Calidad de multimedia saliente</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.
@@ -511,11 +511,11 @@ or the respective page from your chosen <a href="https://chatmail.at/relays">3rd
<p>Los grupos permiten a varias personas chatear juntas en privado con <strong>igualdad de derechos</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 members devices.</p>
<p>Cualquiera puede
cambiar el nombre del grupo o el avatar,
<a href="#addmembers">añadir o eliminar miembros</a>,
establecer la <a href="#ephemeralmsgs">desaparición de mensajes</a>,
y <a href="#edit">eliminar sus propios mensajes</a> de los dispositivos de todos los miembros.</p>
<p>Como todos los miembros tienen los mismos derechos, los grupos funcionan mejor entre <strong>amigos de confianza y familiares</strong>.</p>
@@ -639,86 +639,86 @@ See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number<
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
Canales <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<p>Los canales son una herramienta de difusión de mensajes de uno a muchos.</p>
<h3 id="subscribe-to-a-channel">
<h3 id="suscribirse-a-un-canal">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
Suscribirse a un canal <a href="#suscribirse-a-un-canal" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
<li>Escanea el <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>código QR</strong>
o pulsa el <strong>link de invitación</strong> que has recibido del dueño del canal.</li>
</ul>
<p>Thats all!
You will receive a few of the messages from the channel history
and, from that point on, all new messages from the channel.</p>
<p>¡Eso es todo!
Recibirás unos pocos mensajes del historial del canal
y, desde ese momento, todos los mensajes nuevos del canal.</p>
<p><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p><strong>No te preocupes</strong> si esto no ocurre inmediatamente.
Una vez que el dueño del canal esté online, tu solicitud para unirte será procesada.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Al igual que todo Delta Chat, también los Canales son privados y descentralizados,
no hay modo público de descubrirlo.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<p>Si no quieres compartir tu perfil principal,
puedes crear un <a href="#multiple-accounts">perfil dedicado</a> para unirte a un canal.</p>
<h3 id="create-a-channel">
<h3 id="crear-un-canal">
Create a channel <a href="#create-a-channel" class="anchor"></a>
Crear un canal <a href="#crear-un-canal" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
<p>Pulsa <strong>Nuevo Chat</strong> y elige <strong>Nuevo Canal</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
<p>Introduce un <strong>nombre</strong>, opcionalmente puedes añadir una <strong>imagen</strong> y una <strong>descripción</strong>, y pulsa el botón <strong>Crear</strong>.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
<p>Ahora puedes enviar y gestionar los mensajes como siempre.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
<p>Desde el perfil del canal, <strong>compartir el código QR o el enlace de invitación con otros</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>Los suscriptores recibirán tus mensajes,
pero no podrán enviar mensajes en tu canal.
Cuando se suscriban, recibirán <strong>algunos de los últimos mensajes del historial del canal</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
<h3 id="cuántos-suscriptores-puede-tener-un-canal">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
¿Cuántos suscriptores puede tener un canal? <a href="#cuántos-suscriptores-puede-tener-un-canal" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>Los canales están diseñados para tener audiencias mucho mayores que los <a href="#groups">grupos</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>El límite práctico depende del <a href="#relays">servidor</a> utilizado,
es decir, no hay un número fijo que se aplique siempre.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
@@ -732,69 +732,69 @@ You only have to update the invite link you share with new subscribers in that c
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
Llamadas <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Delta Chat soporta <strong>llamadas</strong> y <strong>videollamadas</strong> individuales.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<p>Las llamadas son compatibles con la versión de escritorio, Ubuntu Touch, iOS y Android 8 o más moderno.</p>
<h3 id="place-a-call">
<h3 id="establecer-una-llamada">
Place a call <a href="#place-a-call" class="anchor"></a>
Establecer una llamada <a href="#establecer-una-llamada" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
<p>En chats individuales, pulsa el 📞 <strong>icono de llamada</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
<p>Esto abre un pequeño menú
donde puedes elegir entre hacer una <strong>Llamada</strong> o una <strong>Videollamada</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
<h3 id="aceptar-o-rechazar-una-llamada">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
Aceptar o rechazar una llamada <a href="#aceptar-o-rechazar-una-llamada" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
<p>Cuando alguien te llama,
Delta Chat muestra una <strong>pantalla de llamada entrante</strong> o una notificación.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
<p>Pulsa <strong>Aceptar</strong> para contestar
o <strong>Rechazar</strong> para rechazar la llamada.</p>
</li>
</ul>
<h3 id="during-a-call">
<h3 id="durante-una-llamada">
During a call <a href="#during-a-call" class="anchor"></a>
Durante una llamada <a href="#durante-una-llamada" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
<p>Puedes <strong>silenciar</strong> tu micrófono.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
<p>Puedes <strong>activar o desactivar tu cámara</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
<p>En móviles, puedes <strong>cambiar entre las cámaras frontal y trasera</strong>.</p>
</li>
</ul>
@@ -802,22 +802,21 @@ or <strong>Decline</strong> to reject the call.</p>
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
<h2 id="llamadas-perdidas-y-notificaciones">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
Llamadas perdidas y notificaciones <a href="#llamadas-perdidas-y-notificaciones" class="anchor"></a>
</h3>
</h2>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
<p>Si no contestas, no oyes la llamada o no tienes el dispositivo a mano, la llamada aparecerá como <strong>llamada perdida</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
<p><strong>Únicamente los contactos que hayas aceptado</strong> pueden hacer que suene tu dispositivo.
Las solicitudes de contacto aparecerán como siempre y no sonarán.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
@@ -1089,7 +1088,7 @@ y espera un momento hasta que aparezca un código QR</p>
<p>En el otro dispositivo, <a href="https://get.delta.chat">instala Delta Chat</a></p>
</li>
<li>
<p>En el segundo dispositivo, abre Delta Chat, selecciona **Añadir desde otro dispositivo y escanea el código QR del primer dispositivo</p>
<p>En el segundo dispositivo, abre Delta Chat, selecciona <strong>Añadir desde otro dispositivo</strong> y escanea el código QR del primer dispositivo</p>
</li>
<li>
<p>La transferencia debería comenzar después de unos segundos y durante la transferencia ambos dispositivos mostrarán el <strong>progreso</strong>.
@@ -1163,7 +1162,7 @@ utiliza la <strong>transferencia manual</strong> descrita debajo</p>
</h3>
<p>This method is only recommended if “Add Second Device” as described above does not work.</p>
<p>Este método solo se recomienda si el método “Añadir un segundo dispositivo” descrito anteriormente no funciona.</p>
<ul>
<li>
@@ -1201,10 +1200,10 @@ o el AppImage para Linux. Puedes encontrarlos en
<a href="https://get.delta.chat">get.delta.chat</a>.</li>
</ul>
<h2 id="advanced">
<h2 id="avanzado">
Advanced <a href="#advanced" class="anchor"></a>
Avanzado <a href="#avanzado" class="anchor"></a>
</h2>
@@ -1212,23 +1211,23 @@ o el AppImage para Linux. Puedes encontrarlos en
<h3 id="experiments">
Experimental Features <a href="#experiments" class="anchor"></a>
Características experimentales <a href="#experiments" class="anchor"></a>
</h3>
<p>At <strong>Settings → Advanced → Experimental Features</strong>
you can try out features we are working on.</p>
<p>En <strong>Ajustes → Avanzado → Características Experimentales</strong>
puedes probar las funcionalidades en las que estamos trabajando.</p>
<p>The features may be <strong>unstable</strong> and may be <strong>changed or removed</strong>.</p>
<p>Estas funcionalidades pueden ser <strong>inestables</strong> y pueden ser <strong>modificadas o eliminadas</strong>.</p>
<p>You can find more information
and give feedback in the <a href="https://support.delta.chat">Forum</a>.</p>
<p>Puedes encontrar más información
y dejar tus comentarios en el <a href="https://support.delta.chat">Foro</a>.</p>
<h3 id="relays">
What are Relays? <a href="#relays" class="anchor"></a>
¿Qué son los servidores? <a href="#relays" class="anchor"></a>
</h3>
@@ -1276,7 +1275,7 @@ you can follow discussions in the <a href="https://support.delta.chat">Forum</a>
</h3>
<p>Yes, but only if the email address is used exclusively by <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p>Sí, pero solo si la dirección de email es utilizada exclusivamente por <a href="https://chatmail.at/clients">clientes de chatmail</a>.</p>
<p>It is not supported to share usage of an email address with non-chatmail apps or web-based mailers,
for the following reasons:</p>
@@ -1300,7 +1299,7 @@ and complicate making chatmail-based messaging more resilient, reliable and fast
<h3 id="classic-email">
How can I configure a chat profile with a classic email address as relay? <a href="#classic-email" class="anchor"></a>
¿Cómo puedo configurar un perfil de chat con una dirección clásica de email como servidor? <a href="#classic-email" class="anchor"></a>
</h3>
@@ -1317,10 +1316,10 @@ A chat profile using a classic email address allows to to send and receive unenc
These messages, and the chats they appear in, are marked with an email icon
<img style="vertical-align:middle; width:1.2em; margin:1px" src="../email-icon.png" alt="email" />.</p>
<h3 id="i-want-to-manage-my-own-server-for-delta-chat-what-do-you-recommend">
<h3 id="quiero-gestionar-mi-propio-servidor-de-delta-chat-qué-me-recomiendan">
I want to manage my own server for Delta Chat. What do you recommend? <a href="#i-want-to-manage-my-own-server-for-delta-chat-what-do-you-recommend" class="anchor"></a>
Quiero gestionar mi propio servidor de Delta Chat. ¿Qué me recomiendan? <a href="#quiero-gestionar-mi-propio-servidor-de-delta-chat-qué-me-recomiendan" class="anchor"></a>
</h3>
@@ -1336,7 +1335,7 @@ that power <a href="https://chatmail.at/clients">chatmail clients</a> of which D
<h3 id="statssending">
What is “Send statistics to Delta Chats developers”? <a href="#statssending" class="anchor"></a>
¿Qué es “Enviar estasticas a los desarrolladores de Delta Chat”? <a href="#statssending" class="anchor"></a>
</h3>
@@ -1355,7 +1354,7 @@ las estadísticas semanales se envían automáticamente a un bot.</p>
<ul>
<li>
<p>How many contacts are introduced by personally scanning a QR code?</p>
<p>¿Cuántos contactos se introducen al escanear personalmente un código QR?</p>
</li>
<li>
<p>¿Qué versiones de Delta Chat están siendo utilizadas?</p>
@@ -1449,7 +1448,7 @@ Messages lacking end-to-end encryption are marked with an email icon
<h3 id="e2eeguarantee">
What does the green checkmark in a contact profile mean? <a href="#e2eeguarantee" class="anchor"></a>
¿Qué significa la marca de verificación verde en el perfil de un contacto? <a href="#e2eeguarantee" class="anchor"></a>
</h3>
@@ -1512,7 +1511,7 @@ which was thankfully adopted in summer 2023.</p>
<h3 id="openpgp-alternatives">
Did you consider using alternatives to OpenPGP for end-to-end-encryption? <a href="#openpgp-alternatives" class="anchor"></a>
¿Considerasteis usar alternativas a OpenPGP para la encriptación de extremo a extremo? <a href="#openpgp-alternatives" class="anchor"></a>
</h3>
@@ -1586,11 +1585,11 @@ even if the message was not end-to-end encrypted.</p>
Delta Chat apps do not store any metadata about contacts or groups on servers, also not in encrypted form.
Instead, all group metadata is end-to-end encrypted and stored on end-user devices, only.</p>
<p>Servers can therefore only see:</p>
<p>Por lo tanto, los servidores solo pueden ver:</p>
<ul>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
<li>Direcciones del emisor y del receptor, generadas aleatoriamente por defecto</li>
<li>Tamaño del mensaje</li>
</ul>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
+48 -49
View File
@@ -29,14 +29,14 @@
<li><a href="#quanti-membri-possono-partecipare-a-un-singolo-gruppo">Quanti membri possono partecipare a un singolo gruppo?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<li><a href="#canali">Canali</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
<li><a href="#iscriversi-a-un-canale">Iscriversi a un canale</a></li>
<li><a href="#creare-un-canale">Creare un canale</a></li>
<li><a href="#quanti-iscritti-può-avere-un-canale">Quanti iscritti può avere un canale?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<li><a href="#calls">Chiamate</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
@@ -635,103 +635,102 @@ ma non è consigliabile superare i 150.</p>
dove Delta Chat è un servizio di messaggistica privato per chattare con <a href="#groups">uguali diritti</a>.
Vedi <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">numero di Dunbar</a> per ulteriori approfondimenti.</p>
<h2 id="channels">
<h2 id="canali">
Channels <a href="#channels" class="anchor"></a>
Canali <a href="#canali" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<p>I canali sono uno strumento da uno a molti per la trasmissione di messaggi.</p>
<h3 id="subscribe-to-a-channel">
<h3 id="iscriversi-a-un-canale">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
Iscriversi a un canale <a href="#iscriversi-a-un-canale" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
<li>Scansiona il <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>Codice QR</strong>
oppure tocca il <strong>collegamento di invito</strong> che hai ricevuto dal proprietario del canale.</li>
</ul>
<p>Thats all!
You will receive a few of the messages from the channel history
and, from that point on, all new messages from the channel.</p>
<p>Ecco fatto!
Riceverai alcuni dei messaggi dalla cronologia del canale
e, da quel momento in poi, tutti i nuovi messaggi dal canale.</p>
<p><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p><strong>Non preoccuparti,</strong> se non accade immediatamente.
Non appena il proprietario del canale si connetterà, la tua richiesta di iscrizione verrà elaborata.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Poiché tutti i Canali di Delta Chat sono privati e decentralizzati,
non esiste una funzione di ricerca pubblica.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>Gli altri iscritti al canale non vedranno che ti sei iscritto e non potranno inviarti messaggi.
Il proprietario del canale, tuttavia, potrà inviarti messaggi.
Inoltre, vedrà che hai letto un messaggio, a meno che tu non abbia disabilitato le conferme di lettura.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<p>Se non desideri condividere il tuo profilo principale,
puoi anche creare un <a href="#multiple-accounts">profilo dedicato</a> per unirti a un canale.</p>
<h3 id="create-a-channel">
<h3 id="creare-un-canale">
Create a channel <a href="#create-a-channel" class="anchor"></a>
Creare un canale <a href="#creare-un-canale" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
<p>Tocca <strong>Nuova Chat</strong> e scegli <strong>Nuovo Canale</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
<p>Inserisci un <strong>nome</strong>, imposta facoltativamente un<strong>immagine</strong> e una <strong>descrizione</strong>, e fai clic sul pulsante <strong>Crea</strong>.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
<p>Ora puoi inviare e gestire i messaggi come di consueto.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
<p>Dal profilo del canale, <strong>condividi il codice QR o il collegamento di invito con altri</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>Gli iscritti riceveranno i tuoi messaggi,
ma non potranno inviare messaggi nel tuo canale.
Al momento delliscrizione, riceveranno <strong>alcuni degli ultimi messaggi della cronologia del canale</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<p>Accanto a ciascun messaggio puoi vedere il <strong>numero di visualizzazioni</strong>.
Tieni presente che questo conteggio si riferisce solo agli abbonati che hanno attivato le conferme di lettura,
quindi il numero reale di visualizzazioni potrebbe essere superiore.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
<h3 id="quanti-iscritti-può-avere-un-canale">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
Quanti iscritti può avere un canale? <a href="#quanti-iscritti-può-avere-un-canale" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>I canali sono progettati per un pubblico molto più ampio rispetto ai <a href="#groups">gruppi</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>Il limite pratico dipende dal numero di <a href="#relays">ripetitori</a> utilizzati, quindi non esiste un singolo numero fisso valido ovunque.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>Per canali molto grandi con diverse decine di migliaia di iscritti,
consigliamo di utilizzare un <a href="#multiple-accounts">profilo dedicato</a> per il canale
e di verificare se il ripetitore è adatto.</p>
<p>But dont be too hesitant: Delta Chat is designed to be relay-agnostic,
so you can change your relay at any point easily -
your existing subscribers will not even notice.
You only have to update the invite link you share with new subscribers in that case.</p>
<p>Ma non esitare troppo: Delta Chat è progettato per essere indipendente dal ripetitore,
quindi puoi cambiare il tuo ripetitore in qualsiasi momento con facilità -
i tuoi iscritti esistenti non se ne accorgeranno nemmeno.
In tal caso, dovrai solo aggiornare il collegamento di invito che condividi con i nuovi iscritti.</p>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
Chiamate <a href="#calls" class="anchor"></a>
</h2>
+84 -135
View File
@@ -16,32 +16,32 @@
<li><a href="#mediaquality">Jak obsługiwana jest jakość multimediów?</a></li>
<li><a href="#ephemeralmsgs">Jak działają znikające wiadomości?</a></li>
<li><a href="#delold">Co się stanie, jeśli włączę opcję „Usuń wiadomości z urządzenia”?</a></li>
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
<li><a href="#remove-account">Jak mogę usunąć swój profil czatu?</a></li>
</ul>
</li>
<li><a href="#groups">Groups</a>
<li><a href="#groups">Grupy</a>
<ul>
<li><a href="#tworzenie-grupy">Tworzenie grupy</a></li>
<li><a href="#addmembers">Add and remove members</a></li>
<li><a href="#addmembers">Dodawanie i usuwanie członków</a></li>
<li><a href="#usunąłem-się-przez-przypadek">Usunąłem się przez przypadek.</a></li>
<li><a href="#nie-chcę-już-otrzymywać-wiadomości-od-grupy">Nie chcę już otrzymywać wiadomości od grupy.</a></li>
<li><a href="#cloning-a-group">Cloning a group</a></li>
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
<li><a href="#klonowanie-grupy">Klonowanie grupy</a></li>
<li><a href="#ilu-członków-może-należeć-do-jednej-grupy">Ilu członków może należeć do jednej grupy?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<li><a href="#channels">Kanały</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
<li><a href="#subskrybowanie-kanału">Subskrybowanie kanału</a></li>
<li><a href="#tworzenie-kanału">Tworzenie kanału</a></li>
<li><a href="#ilu-subskrybentów-może-mieć-kanał">Ilu subskrybentów może mieć kanał?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<li><a href="#calls">Połączenia</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
<li><a href="#nawiązywanie-połączenia">Nawiązywanie połączenia</a></li>
<li><a href="#odbieranie-lub-odrzucanie-połączenia">Odbieranie lub odrzucanie połączenia</a></li>
<li><a href="#w-trakcie-połączenia">W trakcie połączenia</a></li>
<li><a href="#nieodebrane-połączenia-i-powiadomienia">Nieodebrane połączenia i powiadomienia</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
@@ -413,39 +413,28 @@ Poproś partnera czatu o <strong>zeskanowanie</strong> obrazu QR za pomocą apli
<h3 id="remove-account">
How can I delete my chat profile? <a href="#remove-account" class="anchor"></a>
Jak mogę usunąć swój profil czatu? <a href="#remove-account" class="anchor"></a>
</h3>
<p>If you are using more than one chat profile,
you can remove single ones in the top profile switcher menu (on Android and iOS),
or in the sidebar with a right click (in the Desktop app).
Chat profiles are only removed on the device where deletion was triggered.
Chat profiles on other devices will continue to fully function.</p>
<p>Jeśli używasz więcej niż jednego profilu czatu, możesz usunąć pojedyncze profile w górnym menu przełączania profili (na Androidzie i iOS) lub w pasku bocznym, klikając prawym przyciskiem myszy (w aplikacji na komputery). Profile czatu są usuwane tylko na urządzeniu, na którym nastąpiło usunięcie. Profile czatu na innych urządzeniach będą nadal w pełni działać.</p>
<p>If you use a single default chat profile you can simply uninstall the app.
This will still automatically trigger deletion of all associated address data on the chatmail server.
For more info, please refer to <a href="https://nine.testrun.org/info.html#account-deletion">nine.testrun.org address-deletion</a>
or the respective page from your chosen <a href="https://chatmail.at/relays">3rd party chatmail server</a>.</p>
<p>Jeśli używasz jednego domyślnego profilu czatu, możesz po prostu odinstalować aplikację. Spowoduje to automatyczne usunięcie wszystkich powiązanych danych adresowych na serwerze czatu. Aby uzyskać więcej informacji, zapoznaj się z informacjami o <a href="https://nine.testrun.org/info.html#account-deletion">usuwaniu adresów na stronie nine.testrun.org</a> lub odpowiednią stroną wybranego <a href="https://chatmail.at/relays">serwera czatu innej firmy</a>.</p>
<h2 id="groups">
Groups <a href="#groups" class="anchor"></a>
Grupy <a href="#groups" class="anchor"></a>
</h2>
<p>Groups let several people chat together privately with <strong>equal rights</strong>.</p>
<p>Grupy pozwalają kilku osobom na prywatną rozmowę na <strong>równych prawach</strong>.</p>
<p>Anyone can
change the group name or avatar,
<a href="#addmembers">add or remove members</a>,
set <a href="#ephemeralmsgs">disappearing messages</a>,
and <a href="#edit">delete their own messages</a> from all members devices.</p>
<p>Każdy może zmienić nazwę grupy lub awatar, <a href="#addmembers">dodawać lub usuwać członków</a>, ustawiać <a href="#ephemeralmsgs">znikające wiadomości</a> oraz <a href="#edit">usuwać własne wiadomości</a> z urządzeń wszystkich członków.</p>
<p>Because all members have the same rights, groups work best among <strong>trusted friends and family</strong>.</p>
<p>Ponieważ wszyscy członkowie mają te same uprawnienia, grupy najlepiej sprawdzają się w gronie <strong>zaufanych przyjaciół i rodziny</strong>.</p>
<h3 id="tworzenie-grupy">
@@ -470,33 +459,27 @@ and <a href="#edit">delete their own messages</a> from all members devices.</
<h3 id="addmembers">
Add and remove members <a href="#addmembers" class="anchor"></a>
Dodawanie i usuwanie członków <a href="#addmembers" class="anchor"></a>
</h3>
<p>All group members have the <strong>same rights</strong>.
For this reason, everyone can delete any member or add new ones.</p>
<p>Wszyscy członkowie grupy mają <strong>takie same uprawnienia</strong>. Z tego powodu każdy może usunąć dowolnego członka lub dodać nowych.</p>
<ul>
<li>
<p>To <strong>add or delete members</strong>, tap the group name in the chat and select the member to add or remove.</p>
<p>Aby <strong>dodać lub usunąć członków</strong>, dotknij nazwę grupy na czacie i wybierz członka, którego chcesz dodać lub usunąć.</p>
</li>
<li>
<p>If the member is not yet in your contact list, but <strong>face to face</strong> with you,
from the same screen, show a <strong>QR code</strong>.<br />
Ask your chat partner to <strong>scan</strong> the QR image with their Delta Chat app by tapping
<img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> on the main screen.</p>
<p>Jeśli członek nie znajduje się jeszcze na twojej liście kontaktów, ale rozmawiasz z nim <strong>twarzą w twarz</strong>, na tym samym ekranie pokaż mu <strong>kod QR</strong>.
Poproś partnera czatu o <strong>zeskanowanie</strong> obrazu QR za pomocą aplikacji Delta Chat, dotykając <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> na ekranie głównym.</p>
</li>
<li>
<p>For a <strong>remote</strong> member addition,
click “Copy” or “Share” and send the <strong>invite link</strong>
through another private chat to the new member.</p>
<p>Aby dodać członka <strong>zdalnie</strong>, naciśnij „Kopiuj” lub „Udostępnij” i wyślij <strong>link zaproszenia</strong> nowemu członkowi za pośrednictwem innego prywatnego czatu.</p>
</li>
</ul>
<p>QR code and invite link can be used to add several members.
However, since groups are <a href="#groups">meant for trusted people</a>, avoid sharing them publicly.</p>
<p>Kod QR i link zaproszenia można wykorzystać do dodania kilku członków. Ponieważ jednak grupy są <a href="#groups">przeznaczone dla zaufanych osób</a>, unikaj udostępniania ich publicznie.</p>
<h3 id="usunąłem-się-przez-przypadek">
@@ -523,233 +506,199 @@ Jeśli później będziesz chciał ponownie dołączyć do grupy, poproś innego
<p>Alternatywnie możesz też „Wyłączyć powiadomienia” dla grupy, dzięki temu otrzymasz wszystkie wiadomości i nadal będziesz mógł pisać, ale nie będziesz już powiadamiany o żadnych nowych wiadomościach.</p>
<h3 id="cloning-a-group">
<h3 id="klonowanie-grupy">
Cloning a group <a href="#cloning-a-group" class="anchor"></a>
Klonowanie grupy <a href="#klonowanie-grupy" class="anchor"></a>
</h3>
<p>You can duplicate a group to start a separate discussion
or to exclude members without them noticing.</p>
<p>Możesz zduplikować grupę, aby rozpocząć osobną dyskusję lub wykluczyć członków bez ich wiedzy.</p>
<ul>
<li>
<p>Open the group profile and tap <strong>Clone Chat</strong> (Android/iOS),
or right-click the group in the chat list (Desktop).</p>
<p>Otwórz profil grupy i dotknij opcji <strong>Klonuj czat</strong> (Android/iOS) lub kliknij prawym przyciskiem myszy grupę na liście czatów (komputer).</p>
</li>
<li>
<p>Set a new name, choose an avatar, and adjust the member list if needed.</p>
<p>Ustaw nową nazwę, wybierz awatar i w razie potrzeby dostosuj listę członków.</p>
</li>
</ul>
<p>The new group is <strong>fully independent</strong> from the original,
which continues to work as before.</p>
<p>Nowa grupa jest <strong>w pełni niezależna</strong> od oryginalnej, która nadal działa jak dotychczas.</p>
<h3 id="how-many-members-can-participate-in-a-single-group">
<h3 id="ilu-członków-może-należeć-do-jednej-grupy">
How many members can participate in a single group? <a href="#how-many-members-can-participate-in-a-single-group" class="anchor"></a>
Ilu członków może należeć do jednej grupy? <a href="#ilu-członków-może-należeć-do-jednej-grupy" class="anchor"></a>
</h3>
<p>There is no strict technical limit,
but more than 150 is not recommended.</p>
<p>Nie ma ścisłego limitu technicznego, ale nie zaleca się przekraczania 150 osób.</p>
<p>As groups get larger, they can become socially unstable and may need a hierarchy -
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<p>W miarę jak grupy się rozrastają, mogą stawać się niestabilne społecznie i wymagać hierarchii gdzie Delta Chat pełni rolę prywatnego komunikatora do czatowania na <a href="#groups">równych prawach</a>. Więcej informacji znajdziesz w artykule <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Liczba Dunbara</a>.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
Kanały <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<p>Kanały to narzędzie typu jeden do wielu, służące do nadawania wiadomości.</p>
<h3 id="subscribe-to-a-channel">
<h3 id="subskrybowanie-kanału">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
Subskrybowanie kanału <a href="#subskrybowanie-kanału" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
<li>Zeskanuj <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>kod QR</strong> lub naciśnij link zaproszenia otrzymany od właściciela kanału.</li>
</ul>
<p>Thats all!
You will receive a few of the messages from the channel history
and, from that point on, all new messages from the channel.</p>
<p>To wszystko! Otrzymasz kilka wiadomości z historii kanału, a od tego momentu wszystkie nowe wiadomości z kanału.</p>
<p><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p><strong>Nie martw się</strong>, jeśli to nie nastąpi od razu. Gdy właściciel kanału będzie online, twoja prośba o dołączenie zostanie przetworzona.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Podobnie jak cała platforma Delta Chat, również kanały są prywatne i zdecentralizowane, dlatego nie ma możliwości publicznego ujawnienia.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>Inni subskrybenci kanału nie zobaczą, że go subskrybujesz i nie będą mogli wysyłać ci wiadomości. Właściciel kanału może jednak napisać do ciebie. Zobaczy również, że przeczytałeś wiadomość, chyba że masz wyłączone potwierdzenia odczytu.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<p>Jeśli nie chcesz udostępniać swojego głównego profilu, możesz również utworzyć <a href="#multiple-accounts">dedykowany profil</a> do dołączenia do kanału.</p>
<h3 id="create-a-channel">
<h3 id="tworzenie-kanału">
Create a channel <a href="#create-a-channel" class="anchor"></a>
Tworzenie kanału <a href="#tworzenie-kanału" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
<p>Naciśnij <strong>Nowy cza</strong> i wybierz <strong>Nowy kanał</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
<p>Wprowadź <strong>nazwę</strong>, opcjonalnie ustaw <strong>obraz</strong> i <strong>opis</strong>, a następnie naciśnij przycisk <strong>Utwórz</strong>.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
<p>Możesz teraz wysyłać i zarządzać wiadomościami jak zwykle.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
<p>Z profilu kanału <strong>udostępnij kod QR lub link zaproszenia innym osobom</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>Subskrybenci będą otrzymywać twoje wiadomości, ale nie będą mogli wysyłać wiadomości na twoim kanale. Po zasubskrybowaniu otrzymają <strong>kilka najnowszych wiadomości z historii kanału</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<p>Obok każdej wiadomości jest widoczna <strong>liczba wyświetleń</strong>. Pamiętaj, że uwzględnia ona tylko subskrybentów z włączonymi potwierdzeniami odczytu, więc rzeczywista liczba wyświetleń może być wyższa.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
<h3 id="ilu-subskrybentów-może-mieć-kanał">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
Ilu subskrybentów może mieć kanał? <a href="#ilu-subskrybentów-może-mieć-kanał" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>Kanały są przeznaczone dla znacznie większej publiczności niż <a href="#groups">grupy</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>Praktyczny limit zależy od używanego <a href="#relays">przekaźnika</a>, więc nie ma jednej, stałej liczby, która obowiązywałaby wszędzie.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>W przypadku naprawdę dużych kanałów z dziesiątkami tysięcy subskrybentów zalecamy użycie <a href="#multiple-accounts">dedykowanego profilu</a> dla kanału i sprawdzenie, czy przekaźnik jest odpowiedni.</p>
<p>But dont be too hesitant: Delta Chat is designed to be relay-agnostic,
so you can change your relay at any point easily -
your existing subscribers will not even notice.
You only have to update the invite link you share with new subscribers in that case.</p>
<p>Ale nie wahaj się zbytnio: Delta Chat został zaprojektowany tak, aby nie był zależny od przekaźnika, więc możesz go łatwo zmienić w dowolnym momencie twoi obecni subskrybenci nawet tego nie zauważą. W takim przypadku wystarczy zaktualizować link zaproszenia udostępniany nowym subskrybentom.</p>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
Połączenia <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Delta Chat umożliwia indywidualne <strong>połączenia audio i wideo</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<p>Połączenia są obsługiwane na komputerach stacjonarnych, Ubuntu Touch, iOS oraz Androidzie 8 i nowszych.</p>
<h3 id="place-a-call">
<h3 id="nawiązywanie-połączenia">
Place a call <a href="#place-a-call" class="anchor"></a>
Nawiązywanie połączenia <a href="#nawiązywanie-połączenia" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
<p>W czacie indywidualnym dotknij <strong>ikony połączenia</strong> 📞.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
<p>Otworzy się małe menu, w którym możesz wybrać, czy chcesz nawiązać <strong>połączenie audio</strong>, czy <strong>wideo</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
<h3 id="odbieranie-lub-odrzucanie-połączenia">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
Odbieranie lub odrzucanie połączenia <a href="#odbieranie-lub-odrzucanie-połączenia" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
<p>Gdy ktoś do ciebie dzwoni, Delta Chat wyświetla <strong>ekran połączenia przychodzącego</strong> lub powiadomienie.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
<p>Dotknij <strong>Akceptuj</strong>, aby odebrać, lub <strong>Odrzuć</strong>, aby odrzucić połączenie.</p>
</li>
</ul>
<h3 id="during-a-call">
<h3 id="w-trakcie-połączenia">
During a call <a href="#during-a-call" class="anchor"></a>
W trakcie połączenia <a href="#w-trakcie-połączenia" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
<p>Możesz <strong>wyciszyć</strong> mikrofon.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
<p>Możesz <strong>włączyć lub wyłączyć kamerę</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
<p>Na urządzeniach mobilnych możesz <strong>przełączać się między przednią i tylną kamerą</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<p>W zależności od urządzenia możesz również wybrać wyjście audio lub skorzystać z trybu obrazu w obrazie. Na komputerach stacjonarnych połączenie jest wyświetlane w dedykowanym oknie i możesz kontynuować korzystanie z głównego okna Delta Chat jak zwykle.</p>
<h3 id="missed-calls-and-notifications">
<h3 id="nieodebrane-połączenia-i-powiadomienia">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
Nieodebrane połączenia i powiadomienia <a href="#nieodebrane-połączenia-i-powiadomienia" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
<p>Jeśli nie odbierzesz, nie usłyszysz dzwonka lub nie będziesz mieć urządzenia pod ręką, połączenie zostanie oznaczone jako <strong>nieodebrane</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
<p><strong>Tylko zaakceptowane przez ciebie kontakty</strong> mogą wywołać dzwonek na urządzeniu. Prośby o kontakt będą wyświetlane normalnie i nie będą dzwonić.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
<p>W <strong>Ustawienia → Powiadomienia → Połączenia</strong> możesz całkowicie wyłączyć specjalny ekran dzwonka. Jeśli to zrobisz, nie będzie ci przeszkadzał żaden dzwonek, a połączenie nadal będzie można odebrać, dotykając w czacie dymku wiadomości o przychodzącym połączeniu.</p>
</li>
</ul>
+95 -95
View File
@@ -29,19 +29,19 @@
<li><a href="#сколько-участников-может-быть-в-одной-группе">Сколько участников может быть в одной группе?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<li><a href="#channels">Каналы</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
<li><a href="#подписка-на-канал">Подписка на канал</a></li>
<li><a href="#создание-канала">Создание канала</a></li>
<li><a href="#какое-максимальное-количество-подписчиков-может-быть-у-канала">Какое максимальное количество подписчиков может быть у канала?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<li><a href="#calls">Звонки</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
<li><a href="#как-сделать-звонок">Как сделать звонок</a></li>
<li><a href="#принять-или-отклонить-вызов">Принять или отклонить вызов</a></li>
<li><a href="#во-время-звонка">Во время звонка</a></li>
<li><a href="#пропущенные-вызовы-и-уведомления">Пропущенные вызовы и уведомления</a></li>
</ul>
</li>
<li><a href="#webxdc">Встроенные приложения чата</a>
@@ -640,191 +640,191 @@
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
Каналы <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<p>Каналы представляют собой инструмент типа “один-ко-многим” для трансляции сообщений.</p>
<h3 id="subscribe-to-a-channel">
<h3 id="подписка-на-канал">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
Подписка на канал <a href="#подписка-на-канал" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
<li>Отсканируйте <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR-код</strong>
или нажмите на ссылку-приглашение, которую вы получили от владельца канала.</li>
</ul>
<p>Thats all!
You will receive a few of the messages from the channel history
and, from that point on, all new messages from the channel.</p>
<p>Всё готово!
Сначала вы получите несколько сообщений из истории канала,
а затем — все новые сообщения, поступающие в него.</p>
<p><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p><strong>Не беспокойтесь,</strong> если это произойдет не сразу.
Как только владелец канала выйдет в сеть, ваш запрос на вступление будет обработан.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Как и весь Delta Chat, каналы являются приватными и децентрализованными,
поэтому возможность публичного поиска каналов отсутствует.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>Другие подписчики канала не увидят факта вашей подписки и не смогут отправлять вам сообщения.
Однако, владелец канала сможет отправить вам сообщение.
Также он будет видеть, что вы прочитали сообщение, если только вы не отключите подтверждение о прочтении.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<p>Если вы не хотите использовать свой основной профиль,
вы можете создать <a href="#multiple-accounts">специальный профиль</a> для подписки на канал.</p>
<h3 id="create-a-channel">
<h3 id="создание-канала">
Create a channel <a href="#create-a-channel" class="anchor"></a>
Создание канала <a href="#создание-канала" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
<p>Нажмите <strong>Новый чат</strong> и выберите <strong>Новый канал</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
<p>Введите <strong>название</strong>, по желанию добавьте <strong>изображение</strong> и <strong>описание</strong>, а затем нажмите кнопку <strong>Создать</strong>.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
<p>Теперь вы можете отправлять сообщения и управлять ими в обычном режиме.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
<p>В профиле канала вы можете <strong>поделиться QR-кодом или ссылкой-приглашением с другими пользователями</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>Подписчики будут получать ваши сообщения,
но не смогут отправлять сообщения в вашем канале.
При подписке они получат <strong>несколько последних сообщений из истории канала</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<p>Рядом с каждым сообщением вы можете увидеть <strong>количество просмотров</strong>.
Обратите внимание, что учитываются только те подписчики, у которых включены уведомления о прочтении,
поэтому реальное количество просмотров может быть больше.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
<h3 id="какое-максимальное-количество-подписчиков-может-быть-у-канала">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
Какое максимальное количество подписчиков может быть у канала? <a href="#какое-максимальное-количество-подписчиков-может-быть-у-канала" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>Каналы предназначены для гораздо более широкой аудитории, чем <a href="#groups">группы</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>Практический предел зависит от используемого <a href="#relays">релея</a>,
поэтому не существует единого фиксированного значения, применимого во всех случаях.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>Для крайне крупных каналов с десятками тысяч подписчиков,
мы рекомендуем использовать <a href="#multiple-accounts">специальный профиль</a> для управления каналом,
а также предварительно проверить пригодность релея.</p>
<p>But dont be too hesitant: Delta Chat is designed to be relay-agnostic,
so you can change your relay at any point easily -
your existing subscribers will not even notice.
You only have to update the invite link you share with new subscribers in that case.</p>
<p>Не стоит опасаться: архитектура Delta Chat позволяет использовать любой релей (relay-agnostic),
поэтому вы можете легко изменить его в любой момент -
ваши текущие подписчики этого даже не заметят.
В этом случае достаточно будет обновить ссылку-приглашение, которую вы передаёте новым пользователям.</p>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
Звонки <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Delta Chat поддерживает <strong>аудио-</strong> и <strong>видеозвонки</strong> в режиме “один-на-один”.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<p>Звонки работают на ПК, Ubuntu Touch, iOS и Android версии 8 и новее.</p>
<h3 id="place-a-call">
<h3 id="как-сделать-звонок">
Place a call <a href="#place-a-call" class="anchor"></a>
Как сделать звонок <a href="#как-сделать-звонок" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
<p>В чате “один-на-один” нажмите на 📞 <strong>значок вызова</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
<p>Откроется небольшое меню
в котором вы сможете выбрать вид связи <strong>аудио-</strong> или <strong>видеозвонок</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
<h3 id="принять-или-отклонить-вызов">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
Принять или отклонить вызов <a href="#принять-или-отклонить-вызов" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
<p>При входящем звонке,
Delta Chat показывает <strong>экран входящего вызова</strong> или уведомление.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
<p>Нажмите <strong>Принять</strong> чтобы ответить
или <strong>Отклонить</strong> чтобы сбросить звонок.</p>
</li>
</ul>
<h3 id="during-a-call">
<h3 id="во-время-звонка">
During a call <a href="#during-a-call" class="anchor"></a>
Во время звонка <a href="#во-время-звонка" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
<p>Вы можете <strong>отключить</strong> звук микрофона.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
<p>Вы можете <strong>включить или выключить камеру</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
<p>На мобильных устройствах можно <strong>переключаться между фронтальной и основной камерами</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<p>В зависимости от устройства вы можете выбрать источник аудиовыхода или использовать режим “картинка в картинке”.
В приложении для ПК звонок осуществляется в отдельном окне,
что позволяет продолжать работу в основном окне Delta Chat в обычном режиме.</p>
<h3 id="missed-calls-and-notifications">
<h3 id="пропущенные-вызовы-и-уведомления">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
Пропущенные вызовы и уведомления <a href="#пропущенные-вызовы-и-уведомления" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
<p>Если вы не ответите на звонок, не услышите сигнал или ваше устройство будет недоступно,
вызов отобразится как <strong>пропущенный</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
<p><strong>Только ваши подтвержденные контакты</strong> могут заставить ваше устройство звонить.
Запросы на добавление в контакты будут приходить как обычно, но вызова не будет.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
<p>В разделе <strong>Настройки → Уведомления → Звонки</strong>,
вы можете полностью отключить специальный экран входящего вызова.
Если вы это сделаете, никакие уведомления о звонках не будут вас беспокоить;
при этом вы всё равно сможете принять звонок, нажав на иконку сообщения о входящем звонке в соответствующем чате.</p>
</li>
</ul>
@@ -1624,20 +1624,20 @@ Delta Chat вместо этого использует реализацию Ope
</h3>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>Используемым <a href="#relays">релеям</a> необходимо знать ваш IP-адрес,
а в некоторых случаях — данные устройств ваших контактов, если вы совершаете <a href="#calls">вызов</a>
или совместно используете <a href="#webxdc">приложения</a>.</p>
<p>IP Addresses are needed for connectivity and efficiency.
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>IP-адреса необходимы для обеспечения связи и эффективной работы.
Delta Chat не сохраняет их и не раскрывает третьим лицам.
Обратите внимание, что IP-адрес
— это не тот же адрес, который вы указываете службе доставки,
он, как правило, менее точен и зачастую позволяет определить лишь город или регион.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<p>Если вы считаете свой IP-адрес зоной риска,
мы рекомендуем использовать VPN для всей системы.
Настройка VPN для отдельных приложений оставляет уязвимости в общей защите устройства.
Например, нажатие на ссылку может раскрыть ваш IP-адрес неизвестным сторонам, что представляет собой гораздо больший риск.</p>
<h3 id="sealedsender">
+14 -27
View File
@@ -3,8 +3,8 @@
<li><a href="#що-таке-delta-chat">Що таке Delta Chat?</a>
<ul>
<li><a href="#howtoe2ee">Як мені знайти людей для спілкування?</a></li>
<li><a href="#why-is-a-chat-marked-as-request">Why is a chat marked as “Request”?</a></li>
<li><a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other">How can I put two of my friends in contact with each other?</a></li>
<li><a href="#чому-чат-позначений-як-запит">Чому чат позначений як «Запит»?</a></li>
<li><a href="#як-я-можу-познайомити-двох-своїх-друзів-один-з-одним">Як я можу познайомити двох своїх друзів один з одним?</a></li>
<li><a href="#multiple-accounts">Що таке профілі? Як я можу перемикатися між ними?</a></li>
<li><a href="#хто-бачить-моє-зображення-профілю">Хто бачить моє зображення профілю?</a></li>
<li><a href="#signature">Чи можу я встановити біографію/статус у Delta Chat?</a></li>
@@ -166,8 +166,7 @@
<ul>
<li>
<p>If both sides are online, they will soon see a chat
and can start messaging securely.</p>
<p>Якщо обидві сторони перебувають у мережі, незабаром з’явиться вікно чату, і вони зможуть безпечно обмінюватися повідомленнями.</p>
</li>
<li>
<p>If one side is offline or in bad network,
@@ -179,19 +178,17 @@ the ability to chat is delayed until connectivity is restored.</p>
Тепер Ви автоматично використовуватимете <a href="#e2ee">наскрізне шифрування</a> з цим контактом.
Якщо ви додасте один одного у <a href="#groups">групи</a>, наскрізне шифрування буде встановлено між усіма учасниками.</p>
<h3 id="why-is-a-chat-marked-as-request">
<h3 id="чому-чат-позначений-як-запит">
Why is a chat marked as “Request”? <a href="#why-is-a-chat-marked-as-request" class="anchor"></a>
Чому чат позначений як «Запит»? <a href="#чому-чат-позначений-як-запит" class="anchor"></a>
</h3>
<p>As being a private messenger,
only friends and family you <a href="#howtoe2ee">share your QR code or invite link with</a> can write to you.</p>
<p>Оскільки це приватний месенджер, лише друзі та родичі, яким ви <a href="#howtoe2ee">надіслали свій QR-код або посилання-запрошення</a>, можуть вам писати.</p>
<p>Your friends may share your contact with other friends,
this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Request</b></p>
<p>Ваші друзі можуть поділитися вашими контактними даними з іншими друзями, це відображається як <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Запит</b></p>
<ul>
<li>
@@ -205,19 +202,17 @@ this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-s
</li>
</ul>
<h3 id="how-can-i-put-two-of-my-friends-in-contact-with-each-other">
<h3 id="як-я-можу-познайомити-двох-своїх-друзів-один-з-одним">
How can I put two of my friends in contact with each other? <a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other" class="anchor"></a>
Як я можу познайомити двох своїх друзів один з одним? <a href="#як-я-можу-познайомити-двох-своїх-друзів-один-з-одним" class="anchor"></a>
</h3>
<p>Attach the first contact to the chat of the second using <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attachment Button → Contact</strong>.
You can also add a little introduction message.</p>
<p>Додайте перший контакт до чату другого, скориставшись функцією <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Кнопка «Додати» → Контакт</strong>. Ви також можете додати коротке повідомлення-представлення.</p>
<p>The second contact will receive a <strong>card</strong> then
and can tap it to start chatting with the first contact.</p>
<p>Тоді другий контакт отримає <strong>картку</strong> і зможе натиснути на неї, щоб почати чат із першим контактом.</p>
<h3 id="multiple-accounts">
@@ -227,9 +222,7 @@ and can tap it to start chatting with the first contact.</p>
</h3>
<p>A profile is <strong>a name, a picture</strong> and some additional information for encrypting messages.
A profile lives on your device(s) only
and uses the server only to relay messages.</p>
<p>Профіль — це <strong>ім’я, зображення</strong> та деяка додаткова інформація для шифрування повідомлень. Профіль зберігається виключно на вашому пристрої (пристроях) і використовує сервер лише для передачі повідомлень.</p>
<p>Під час першого встановлення Delta Chat створюється перший профіль.</p>
@@ -259,10 +252,7 @@ and uses the server only to relay messages.</p>
</h3>
<p>Yes,
you can do so under <strong>Settings → Profile → Bio</strong>.
Once you sent a message to a contact,
they will see it when they view your contact details.</p>
<p>Так, ви можете це зробити в розділі <strong>Налаштування → Профіль → Опис</strong>. Після того як ви надішлете повідомлення контакту, він побачить його, переглянувши ваші контактні дані.</p>
<h3 id="що-значить-закріплення-приглушення-архівування">
@@ -330,10 +320,7 @@ they will see it when they view your contact details.</p>
</h3>
<p>You can sometimes see a <strong>green dot</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />
next to the avatar of a contact.
It means they were <strong>recently seen by you</strong> in the last 10 minutes,
e.g. because they messaged you or sent a read receipt.</p>
<p>Іноді біля аватара контакту можна побачити <strong>зелену крапку</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />. Це означає, що ви <strong>нещодавно бачили його</strong> протягом останніх 10 хвилин, наприклад, тому що він надіслав вам повідомлення або підтвердження прочитання.</p>
<p>So this is not a real time online status
and others will as well not always see that you are “online”.</p>
-3
View File
@@ -719,9 +719,6 @@ public class Rpc {
* because the word "channel" already appears a lot in the code,
* which would make it hard to grep for it.
* <p>
* After creation, the chat contains no recipients and is in _unpromoted_ state;
* see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
* <p>
* Returns the created chat's id.
*/
public Integer createBroadcast(Integer accountId, String chatName) throws RpcException {
@@ -2,6 +2,8 @@
package chat.delta.rpc.types;
public class WebxdcMessageInfo {
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String orientation;
/** if the Webxdc represents a document, then this is the name of the document */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String document;
@@ -84,7 +84,7 @@ public class DcAccounts {
public boolean isAllChatmail() {
for (int accountId : getAll()) {
DcContext dcContext = getAccount(accountId);
if (!dcContext.isChatmail()) {
if (dcContext.getConfigInt("is_chatmail") == 0) {
return false;
}
}
@@ -362,10 +362,6 @@ public class DcContext {
return displayname;
}
public boolean isChatmail() {
return getConfigInt("is_chatmail") == 1;
}
public boolean isMuted() {
return getConfigInt("is_muted") == 1;
}
@@ -302,7 +302,10 @@ public class ApplicationContext extends MultiDexApplication {
Log.i(
"DeltaChat",
"++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
getDcAccounts().maybeNetwork();
// onBlockedStatusChanged is only available on API 29+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getDcAccounts().maybeNetwork();
}
}
@Override
@@ -310,8 +313,13 @@ public class ApplicationContext extends MultiDexApplication {
@NonNull android.net.Network network, boolean blocked) {
Log.i(
"DeltaChat",
"++++++++++++++++++ NetworkCallback.onBlockedStatusChanged() #"
"++++++++++++++++++ NetworkCallback.onBlockedStatusChanged("
+ blocked
+ ") #"
+ debugOnBlockedStatusChangedCount++);
if (!blocked) {
getDcAccounts().maybeNetwork();
}
}
@Override
@@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.connect.DirectShareUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.thoughtcrime.securesms.util.views.ProgressDialog;
@@ -488,10 +487,6 @@ public abstract class BaseConversationListFragment extends Fragment implements A
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
if (isRelayingMessageContent(getActivity())) {
if (ShareUtil.getSharedContactId(getActivity()) != 0) {
return false; // no sharing of a contact to multiple recipients at the same time, we can
// reconsider when that becomes a real-world need
}
Context context = getContext();
if (context != null) {
fab.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_send_sms_white_24dp));
@@ -168,6 +168,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int RECORD_VIDEO = 8;
private static final int PICK_WEBXDC = 9;
private static final Object searchLock = new Object();
private GlideRequests glideRequests;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
@@ -176,7 +178,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected ConversationTitleView titleView;
private ConversationFragment fragment;
private InputAwareLayout container;
private View composePanel;
private ScaleStableImageView backgroundView;
private MessageRequestsBottomView messageRequestBottomView;
private ProgressDialog progressDialog;
@@ -263,6 +264,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void handleOnBackPressed() {
if (container.isInputOpen()) {
container.hideCurrentInput(composeText);
} else if (searchMenu != null) {
searchCollapse();
} else {
handleReturnToConversationList();
}
@@ -279,6 +282,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
eventCenter.removeObservers(this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_DELETED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
@@ -386,7 +390,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected void onPause() {
super.onPause();
processComposeControls(ACTION_SAVE_DRAFT);
if (inputPanel.isRecording() && inputPanel.getRecordingDuration() > 1000) {
saveRecording();
} else {
processComposeControls(ACTION_SAVE_DRAFT);
}
DcHelper.getNotificationCenter(this).clearVisibleChat();
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
@@ -439,32 +447,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
else mediaType = MediaType.IMAGE;
setMedia(singleUri, mediaType);
} else {
final ClipData multipleUris = data.getClipData();
if (multipleUris != null) {
final int uriCount = multipleUris.getItemCount();
if (uriCount > 0) {
ArrayList<Uri> uriList = new ArrayList<>(uriCount);
for (int i = 0; i < uriCount; i++) {
uriList.add(multipleUris.getItemAt(i).getUri());
}
askSendingFiles(
uriList,
() -> {
Util.runOnAnyBackgroundThread(
() -> {
SendRelayedMessageUtil.sendMultipleMsgs(this, chatId, uriList, null);
});
});
}
}
sendMultipleMsgs(data);
}
break;
case PICK_DOCUMENT:
final String docMimeType = MediaUtil.getMimeType(this, data.getData());
final MediaType docMediaType =
MediaUtil.isAudioType(docMimeType) ? MediaType.AUDIO : MediaType.DOCUMENT;
setMedia(data.getData(), docMediaType);
if (data.getData() != null) { // single Uri
final String docMimeType = MediaUtil.getMimeType(this, data.getData());
final MediaType docMediaType =
MediaUtil.isAudioType(docMimeType) ? MediaType.AUDIO : MediaType.DOCUMENT;
setMedia(data.getData(), docMediaType);
} else {
sendMultipleMsgs(data);
}
break;
case PICK_WEBXDC:
@@ -507,6 +502,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void sendMultipleMsgs(Intent data) {
final ClipData multipleUris = data.getClipData();
if (multipleUris != null) {
final int uriCount = multipleUris.getItemCount();
if (uriCount > 0) {
ArrayList<Uri> uriList = new ArrayList<>(uriCount);
for (int i = 0; i < uriCount; i++) {
uriList.add(multipleUris.getItemAt(i).getUri());
}
askSendingFiles(
uriList,
() -> {
Util.runOnAnyBackgroundThread(
() -> SendRelayedMessageUtil.sendMultipleMsgs(this, chatId, uriList, null));
});
}
}
}
@Override
public void startActivity(Intent intent) {
if (intent.getStringExtra(Browser.EXTRA_APPLICATION_ID) != null) {
@@ -712,7 +726,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
}
playbackViewModel.stopNonMessageAudioPlayback();
if (attachmentManager.isAttachmentPresent()) {
SlideDeck slideDeck = attachmentManager.buildSlideDeck();
int audioDraftId = slideDeck.getAudioDraftId();
if (audioDraftId != 0) {
playbackViewModel.stop(audioDraftId);
}
}
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
Intent intent =
@@ -879,13 +899,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void handleSharing() {
ArrayList<Uri> uriList = ShareUtil.getSharedUris(this);
int sharedContactId = ShareUtil.getSharedContactId(this);
if (uriList.size() > 1) {
askSendingFiles(uriList, () -> SendRelayedMessageUtil.immediatelyRelay(this, chatId));
} else {
if (sharedContactId != 0) {
addAttachmentContactInfo(sharedContactId);
} else if (ShareUtil.getSharedHtml(this) != null
if (ShareUtil.getSharedHtml(this) != null
|| ShareUtil.getSharedSubject(this) != null
|| ("sticker".equals(ShareUtil.getSharedType(this)) && !uriList.isEmpty())) {
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
@@ -973,6 +990,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
setMedia(draft, MediaType.GIF).addListener(listener);
break;
case DcMsg.DC_MSG_AUDIO:
case DcMsg.DC_MSG_VOICE:
setMedia(draft, MediaType.AUDIO).addListener(listener);
break;
case DcMsg.DC_MSG_VIDEO:
@@ -1013,7 +1031,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
attachButton = ViewUtil.findById(this, R.id.attach_button);
composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
emojiPickerContainer = ViewUtil.findById(this, R.id.emoji_picker_container);
composePanel = ViewUtil.findById(this, R.id.bottom_panel);
container = ViewUtil.findById(this, R.id.layout_container);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
@@ -1039,7 +1056,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// apply padding top to avoid drawing behind top bar
ViewUtil.applyWindowInsets(findViewById(R.id.fragment_content), false, true, false, false);
// apply padding to root to avoid collision with system bars
ViewUtil.applyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
ViewUtil.applyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
ViewUtil.applyWindowInsets(emojiPickerContainer, false, false, false, true);
container.addOnKeyboardShownListener(this);
container.addOnKeyboardHiddenListener(backgroundView);
@@ -1129,22 +1147,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipient = new Recipient(this, dcChat);
glideRequests = GlideApp.with(this);
setComposePanelVisibility(true);
setInputPanelVisibility(true);
initializeContactRequest();
}
private void setComposePanelVisibility(boolean isInitialization) {
private void setInputPanelVisibility(boolean isInitialization) {
int inputPanelVisibility;
boolean isAttachmentHidden;
if (dcChat.canSend()) {
composePanel.setVisibility(View.VISIBLE);
attachmentManager.setHidden(false);
inputPanelVisibility = View.VISIBLE;
isAttachmentHidden = false;
inputPanel.setSubjectVisible(!dcChat.isEncrypted());
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input
// bar
// ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
// fragment.handleRemoveBottomInsets();
} else {
composePanel.setVisibility(View.GONE);
attachmentManager.setHidden(true);
inputPanelVisibility = View.GONE;
isAttachmentHidden = true;
hideSoftKeyboard();
inputPanel.setSubjectVisible(false);
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input
@@ -1156,6 +1176,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
*/
}
synchronized (searchLock) {
if (searchMenu != null) { // in search mode, don't change visibility directly
beforeSearchInputPanelVisibility = inputPanelVisibility;
beforeSearchAttachmentEditorHidden = isAttachmentHidden;
} else {
inputPanel.setVisibility(inputPanelVisibility);
// attachmentManager.setHidden(isAttachmentHidden);
}
}
}
//////// Helper Methods
@@ -1268,9 +1297,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inputPanel.clearSubject();
}
// Stop draft audio playback regardless, since it is unlikely
// we will need background playback for drafts
playbackViewModel.stopNonMessageAudioPlayback();
// Stop draft audio playback
if (slideDeck != null) {
int audioDraftId = slideDeck.getAudioDraftId();
if (audioDraftId != 0) {
playbackViewModel.stop(audioDraftId);
}
}
DcContext dcContext = DcHelper.getContext(context);
final int currentChatId = dcChat.getId();
@@ -1681,6 +1714,56 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void saveRecording() {
inputPanel.resetRecordingUI();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
final int thisChatId = chatId;
final Optional<QuoteModel> quote = inputPanel.getQuote();
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(
new ListenableFuture.Listener<Pair<Uri, Long>>() {
@Override
public void onSuccess(final @NonNull Pair<Uri, Long> result) {
Util.runOnAnyBackgroundThread(
() -> {
try {
DcContext dcContext = DcHelper.getContext(context);
String path =
DcHelper.copyToBlobdir(
ConversationActivity.this, result.first, "voice", ".m4a");
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_VOICE);
msg.setFileAndDeduplicate(path, null, null);
if (quote.isPresent()) {
msg.setQuote(quote.get().getQuotedMsg());
}
dcContext.setDraft(thisChatId, msg);
} catch (Exception e) {
Log.e(TAG, "Failed to save voice as draft", e);
} finally {
PersistentBlobProvider.getInstance()
.delete(ConversationActivity.this, result.first);
}
runOnUiThread(
() -> {
if (chatId == thisChatId && !isFinishing() && !isDestroyed()) {
initializeDraft();
updateToggleButtonState();
}
});
});
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, "Failed to stop recording", e);
}
});
}
private class AttachButtonListener implements OnClickListener {
@Override
public void onClick(View v) {
@@ -1795,19 +1878,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
dcChat = dcContext.getChat(chatId);
titleView.setTitle(glideRequests, dcChat);
initializeSecurity(isSecureText, isDefaultSms);
setComposePanelVisibility(false);
setInputPanelVisibility(false);
initializeContactRequest();
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|| eventId == DcContext.DC_EVENT_MSG_READ)
&& event.getData1Int() == chatId) {
dcChat = dcContext.getChat(chatId);
titleView.setTitle(glideRequests, dcChat);
} else if (eventId == DcContext.DC_EVENT_CHAT_DELETED && event.getData1Int() == chatId) {
finish();
}
}
// in-chat search
private int beforeSearchComposeVisibility = View.VISIBLE;
private boolean beforeSearchAttachmentEditorHidden;
private int beforeSearchMsgRequestVisibility;
private int beforeSearchInputPanelVisibility;
private Menu searchMenu = null;
private int[] searchResult = {};
@@ -1828,17 +1915,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void searchExpand(final Menu menu, final MenuItem searchItem) {
searchMenu = menu;
synchronized (searchLock) {
searchMenu = menu;
beforeSearchComposeVisibility = composePanel.getVisibility();
composePanel.setVisibility(View.GONE);
beforeSearchAttachmentEditorHidden = attachmentManager.isHidden();
beforeSearchMsgRequestVisibility = messageRequestBottomView.getVisibility();
beforeSearchInputPanelVisibility = inputPanel.getVisibility();
// attachmentManager.setHidden(true);
messageRequestBottomView.setVisibility(View.GONE);
inputPanel.setVisibility(View.GONE);
}
ConversationActivity.this.makeSearchMenuVisible(menu, searchItem);
}
private void searchCollapse() {
searchMenu = null;
composePanel.setVisibility(beforeSearchComposeVisibility);
synchronized (searchLock) {
searchMenu = null;
// attachmentManager.setHidden(beforeSearchAttachmentEditorHidden);
messageRequestBottomView.setVisibility(beforeSearchMsgRequestVisibility);
inputPanel.setVisibility(beforeSearchInputPanelVisibility);
}
// trigger onPrepareOptionsMenu() to restore correct menu visibility
invalidateOptionsMenu();
@@ -1899,16 +1997,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void initializeContactRequest() {
if (!dcChat.isContactRequest()) {
messageRequestBottomView.setVisibility(View.GONE);
synchronized (searchLock) {
if (searchMenu != null) { // in search mode, don't change visibility directly
beforeSearchMsgRequestVisibility = View.GONE;
} else {
messageRequestBottomView.setVisibility(View.GONE);
}
}
return;
}
messageRequestBottomView.setVisibility(View.VISIBLE);
synchronized (searchLock) {
if (searchMenu != null) { // in search mode, don't change visibility directly
beforeSearchMsgRequestVisibility = View.VISIBLE;
} else {
messageRequestBottomView.setVisibility(View.VISIBLE);
}
}
messageRequestBottomView.setAcceptOnClickListener(
v -> {
DcHelper.getContext(context).acceptChat(chatId);
messageRequestBottomView.setVisibility(View.GONE);
composePanel.setVisibility(View.VISIBLE);
inputPanel.setVisibility(View.VISIBLE);
});
if (dcChat.getType() == DcChat.DC_CHAT_TYPE_GROUP) {
@@ -727,6 +727,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
refreshAvatar();
refreshUnreadIndicator();
refreshTitle();
conversationListFragment.resetScrollPosition();
conversationListFragment.loadChatlistAsync();
}
@@ -64,6 +64,7 @@ public class ConversationListFragment extends BaseConversationListFragment
private Timer reloadTimer;
private boolean chatlistJustLoaded;
private boolean reloadTimerInstantly;
private boolean resetScrollPosition;
@Override
public void onCreate(Bundle icicle) {
@@ -240,9 +241,9 @@ public class ConversationListFragment extends BaseConversationListFragment
public void loadChatlistAsync() {
synchronized (loadChatlistLock) {
needsAnotherLoad = true;
if (inLoadChatlist) {
Log.i(TAG, "chatlist loading debounced");
needsAnotherLoad = true;
return;
}
inLoadChatlist = true;
@@ -251,6 +252,9 @@ public class ConversationListFragment extends BaseConversationListFragment
Util.runOnAnyBackgroundThread(
() -> {
while (true) {
Log.i(TAG, "executing debounced chatlist loading");
loadChatlist();
synchronized (loadChatlistLock) {
if (!needsAnotherLoad) {
inLoadChatlist = false;
@@ -259,8 +263,6 @@ public class ConversationListFragment extends BaseConversationListFragment
needsAnotherLoad = false;
}
Log.i(TAG, "executing debounced chatlist loading");
loadChatlist();
Util.sleep(100);
}
});
@@ -302,6 +304,11 @@ public class ConversationListFragment extends BaseConversationListFragment
}
((ConversationListAdapter) list.getAdapter()).changeData(chatlist);
if (resetScrollPosition) {
list.scrollToPosition(0);
resetScrollPosition = false;
}
});
}
@@ -358,4 +365,8 @@ public class ConversationListFragment extends BaseConversationListFragment
loadChatlistAsync();
}
}
public void resetScrollPosition() {
resetScrollPosition = true;
}
}
@@ -7,6 +7,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
@@ -25,8 +26,11 @@ import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ShareUtil;
@@ -36,6 +40,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class ProfileActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate {
private static final String TAG = "ProfileActivity";
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
@@ -398,7 +404,18 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
Intent composeIntent = new Intent();
DcContact dcContact = dcContext.getContact(contactId);
if (dcContact.isKeyContact()) {
ShareUtil.setSharedContactId(composeIntent, contactId);
try {
byte[] vcard =
rpc.makeVcard(rpc.getSelectedAccountId(), Collections.singletonList(contactId))
.getBytes();
Uri vcardUri =
PersistentBlobProvider.getInstance().create(this, vcard, "text/vcard", "contact.vcf");
ArrayList<Uri> uris = new ArrayList<>();
uris.add(vcardUri);
ShareUtil.setSharedUris(composeIntent, uris);
} catch (RpcException e) {
Log.e(TAG, "Failed to create vCard for sharing contactId=" + contactId, e);
}
} else {
ShareUtil.setSharedText(composeIntent, dcContact.getAddr());
}
@@ -260,12 +260,11 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
final String addr = extraEmail[0];
int contactId = dcContext.lookupContactIdByAddr(addr);
if (contactId == 0) {
contactId = dcContext.createContact(null, addr);
if (contactId != 0 || dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
if (contactId == 0) contactId = dcContext.createContact(null, addr);
chatId = dcContext.createChatByContactId(contactId);
accId = dcContext.getAccountId();
}
chatId = dcContext.createChatByContactId(contactId);
accId = dcContext.getAccountId();
}
Intent composeIntent;
if (accId != -1 && chatId > 0) {
@@ -68,6 +68,11 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
}
}
@Override
protected boolean allowInLockedMode() {
return true;
}
@Override
protected void onCreate(Bundle state, boolean ready) {
setContentView(R.layout.web_view_activity);
@@ -34,6 +34,7 @@ import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.WebxdcMessageInfo;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
@@ -50,7 +51,6 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.json.JSONObject;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.IntentUtils;
@@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.util.Util;
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
private static final String TAG = "WebxdcActivity";
private static final String EXTRA_ACCOUNT_ID = "accountId";
private static final String EXTRA_CHAT_ID = "chatId";
private static final String EXTRA_APP_MSG_ID = "appMessageId";
private static final String EXTRA_HIDE_ACTION_BAR = "hideActionBar";
private static final String EXTRA_HREF = "href";
@@ -69,11 +70,15 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
private ValueCallback<Uri[]> filePathCallback;
private DcContext dcContext;
private int accountId;
private Rpc rpc;
private int chatId;
private DcMsg dcAppMsg;
private String baseURL;
private String sourceCodeUrl = "";
private String selfAddr;
private boolean isAppSender;
private boolean isBroadcast;
private int sendUpdateMaxSize;
private int sendUpdateInterval;
private boolean internetAccess = false;
@@ -109,7 +114,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
dcContext.setConfigInt("ui.maps_version", mapsVersion);
}
openWebxdcActivity(context, msgId, true, href);
openWebxdcActivity(context, msgId, chatId, true, href);
}
public static void openWebxdcActivity(Context context, DcMsg instance) {
@@ -117,37 +122,43 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
public static void openWebxdcActivity(Context context, @NonNull DcMsg instance, String href) {
openWebxdcActivity(context, instance.getId(), false, href);
openWebxdcActivity(context, instance.getId(), instance.getChatId(), false, href);
}
public static void openWebxdcActivity(
Context context, int msgId, boolean hideActionBar, String href) {
Context context, int msgId, int chatId, boolean hideActionBar, String href) {
if (!Util.isClickedRecently()) {
context.startActivity(getWebxdcIntent(context, msgId, hideActionBar, href));
context.startActivity(getWebxdcIntent(context, msgId, chatId, hideActionBar, href));
}
}
private static Intent getWebxdcIntent(
Context context, int msgId, boolean hideActionBar, String href) {
Context context, int msgId, int chatId, boolean hideActionBar, String href) {
DcContext dcContext = DcHelper.getContext(context);
int accountId = dcContext.getAccountId();
Intent intent = new Intent(context, WebxdcActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(EXTRA_ACCOUNT_ID, dcContext.getAccountId());
// Unique URI per webxdc instance so FLAG_ACTIVITY_NEW_DOCUMENT can identify the document:
intent.setData(Uri.parse("webxdc://" + accountId + "/" + msgId));
intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
intent.putExtra(EXTRA_CHAT_ID, chatId);
intent.putExtra(EXTRA_APP_MSG_ID, msgId);
intent.putExtra(EXTRA_HIDE_ACTION_BAR, hideActionBar);
intent.putExtra(EXTRA_HREF, href);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
return intent;
}
private static Intent[] getWebxdcIntentWithParentStack(Context context, int msgId) {
DcContext dcContext = DcHelper.getContext(context);
int chatId = dcContext.getMsg(msgId).getChatId();
final Intent chatIntent =
new Intent(context, ConversationActivity.class)
.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId())
.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId)
.setAction(Intent.ACTION_VIEW);
final Intent webxdcIntent = getWebxdcIntent(context, msgId, false, "");
final Intent webxdcIntent = getWebxdcIntent(context, msgId, chatId, false, "");
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(chatIntent)
@@ -193,18 +204,11 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
});
DcEventCenter eventCenter =
DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this);
int appMessageId = b.getInt(EXTRA_APP_MSG_ID);
int accountId = b.getInt(EXTRA_ACCOUNT_ID);
accountId = b.getInt(EXTRA_ACCOUNT_ID);
this.dcContext = DcHelper.getContext(getApplicationContext());
if (accountId != dcContext.getAccountId()) {
AccountManager.getInstance().switchAccount(getApplicationContext(), accountId);
this.dcContext = DcHelper.getContext(getApplicationContext());
this.dcContext = DcHelper.getAccounts(getApplicationContext()).getAccount(accountId);
}
this.dcAppMsg = this.dcContext.getMsg(appMessageId);
@@ -213,6 +217,15 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
finish();
return;
}
chatId = b.getInt(EXTRA_CHAT_ID, dcAppMsg.getChatId());
DcEventCenter eventCenter =
DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_MSG_DELETED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_DELETED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this);
// `msg_id` in the subdomain makes sure, different apps using same files do not share the same
// cache entry
@@ -220,21 +233,32 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
// (a random-id would also work, but would need maintenance and does not add benefits as we
// regard the file-part interceptRequest() only,
// also a random-id is not that useful for debugging)
this.baseURL = "https://acc" + dcContext.getAccountId() + "-msg" + appMessageId + ".localhost";
this.baseURL = "https://acc" + accountId + "-msg" + appMessageId + ".localhost";
final JSONObject info = this.dcAppMsg.getWebxdcInfo();
internetAccess = JsonUtils.optBoolean(info, "internet_access");
if ("landscape".equals(JsonUtils.optString(info, "orientation"))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
// enter fullscreen mode if necessary,
// this is needed here because if the app is opened while already in landscape mode,
// onConfigurationChanged() is not triggered
setScreenMode(getResources().getConfiguration());
WebxdcMessageInfo info;
try {
info = rpc.getWebxdcInfo(accountId, appMessageId);
if ("landscape".equals(info.orientation)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
// enter fullscreen mode if necessary,
// this is needed here because if the app is opened while already in landscape mode,
// onConfigurationChanged() is not triggered
setScreenMode(getResources().getConfiguration());
}
internetAccess = info.internetAccess;
selfAddr = info.selfAddr;
isAppSender = info.isAppSender;
isBroadcast = info.isBroadcast;
sendUpdateMaxSize = info.sendUpdateMaxSize;
sendUpdateInterval = info.sendUpdateInterval;
} catch (RpcException e) { // unexpected error, log it and finish
Log.e(TAG, "RPC Error", e);
finish();
return;
}
selfAddr = info.optString("self_addr");
sendUpdateMaxSize = info.optInt("send_update_max_size");
sendUpdateInterval = info.optInt("send_update_interval");
toggleFakeProxy(!internetAccess);
@@ -315,7 +339,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.menu_add_to_home_screen) {
addToHomeScreen(this, dcAppMsg.getId());
addToHomeScreen(this, dcContext, dcAppMsg.getId());
return true;
} else if (itemId == R.id.webxdc_help) {
DcHelper.openHelp(this, "#webxdc");
@@ -468,6 +492,8 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
@Override
public void handleEvent(@NonNull DcEvent event) {
if (event.getAccountId() != accountId) return;
int eventId = event.getId();
if ((eventId == DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE
&& event.getData1Int() == dcAppMsg.getId())) {
@@ -487,22 +513,26 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
this.dcContext.getMsg(event.getData2Int()); // msg changed, reload data from db
Util.runOnAnyBackgroundThread(
() -> {
final JSONObject info = dcAppMsg.getWebxdcInfo();
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Util.runOnMain(
() -> {
updateTitleAndMenu(info, chat);
});
try {
final WebxdcMessageInfo info =
rpc.getWebxdcInfo(dcContext.getAccountId(), dcAppMsg.getId());
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Util.runOnMain(() -> updateTitleAndMenu(info, chat));
} catch (RpcException e) {
Log.e(TAG, "RPC Error", e);
}
});
} else if ((eventId == DcContext.DC_EVENT_MSG_DELETED
&& event.getData2Int() == dcAppMsg.getId())
|| (eventId == DcContext.DC_EVENT_CHAT_DELETED && event.getData1Int() == chatId)) {
finish();
}
}
private void updateTitleAndMenu(JSONObject info, DcChat chat) {
final String docName = JsonUtils.optString(info, "document");
final String xdcName = JsonUtils.optString(info, "name");
final String currSourceCodeUrl = JsonUtils.optString(info, "source_code_url");
getSupportActionBar()
.setTitle((docName.isEmpty() ? xdcName : docName) + " " + chat.getName());
private void updateTitleAndMenu(WebxdcMessageInfo info, DcChat chat) {
final String docName = TextUtils.isEmpty(info.document) ? info.name : info.document;
getSupportActionBar().setTitle(docName + " " + chat.getName());
String currSourceCodeUrl = info.sourceCodeUrl != null ? info.sourceCodeUrl : "";
if (!sourceCodeUrl.equals(currSourceCodeUrl)) {
sourceCodeUrl = currSourceCodeUrl;
invalidateOptionsMenu();
@@ -511,6 +541,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
private void showInChat() {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accountId);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcAppMsg.getChatId());
intent.putExtra(
ConversationActivity.STARTING_POSITION_EXTRA,
@@ -519,27 +550,31 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
public static void addToHomeScreen(Activity activity, int msgId) {
addToHomeScreen(activity, DcHelper.getContext(activity), msgId);
}
public static void addToHomeScreen(Activity activity, DcContext dcContext, int msgId) {
Context context = activity.getApplicationContext();
try {
DcContext dcContext = DcHelper.getContext(context);
Rpc rpc = DcHelper.getRpc(context);
int accountId = dcContext.getAccountId();
DcMsg msg = dcContext.getMsg(msgId);
final JSONObject info = msg.getWebxdcInfo();
WebxdcMessageInfo info = rpc.getWebxdcInfo(accountId, msgId);
final String docName = JsonUtils.optString(info, "document");
final String xdcName = JsonUtils.optString(info, "name");
byte[] blob = msg.getWebxdcBlob(JsonUtils.optString(info, "icon"));
final String docName = TextUtils.isEmpty(info.document) ? info.name : info.document;
byte[] blob = msg.getWebxdcBlob(info.icon);
ByteArrayInputStream is = new ByteArrayInputStream(blob);
BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon");
Bitmap bitmap = drawable.getBitmap();
ShortcutInfoCompat shortcutInfoCompat =
new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId)
.setShortLabel(docName.isEmpty() ? xdcName : docName)
new ShortcutInfoCompat.Builder(context, "xdc-" + accountId + "-" + msgId)
.setShortLabel(docName)
.setIcon(
IconCompat.createWithBitmap(
bitmap)) // createWithAdaptiveBitmap() removes decorations but cuts out a too
// small circle and defamiliarize the icon too much
.setIntents(getWebxdcIntentWithParentStack(context, msgId))
.setIntent(getWebxdcIntent(context, msgId, msg.getChatId(), false, ""))
.build();
Toast.makeText(context, R.string.one_moment, Toast.LENGTH_SHORT).show();
@@ -631,6 +666,16 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
return WebxdcActivity.this.dcContext.getName();
}
@JavascriptInterface
public boolean isAppSender() {
return WebxdcActivity.this.isAppSender;
}
@JavascriptInterface
public boolean isBroadcast() {
return WebxdcActivity.this.isBroadcast;
}
/**
* @noinspection unused
*/
@@ -1,10 +1,14 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -71,6 +75,11 @@ public class WelcomeActivity extends BaseActionBarActivity
.setOnClickListener((v) -> showSignInDialogWithPermission());
findViewById(R.id.backup_button).setOnClickListener((v) -> startImportBackup());
AnimatorSet floating =
(AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.floating_logo);
floating.setTarget(findViewById(R.id.welcome_icon));
floating.start();
registerForEvents();
initializeActionBar();
@@ -120,7 +129,15 @@ public class WelcomeActivity extends BaseActionBarActivity
boolean canGoBack = AccountManager.getInstance().canRollbackAccountCreation(this);
supportActionBar.setDisplayHomeAsUpEnabled(canGoBack);
getSupportActionBar().setTitle(canGoBack ? R.string.add_account : R.string.app_name);
if (canGoBack) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
supportActionBar.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
supportActionBar.setElevation(0);
}
supportActionBar.setTitle(R.string.add_account);
} else {
supportActionBar.hide();
}
}
private void registerForEvents() {
@@ -189,7 +206,12 @@ public class WelcomeActivity extends BaseActionBarActivity
File imexDir = DcHelper.getImexDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AttachmentManager.selectMediaType(
this, "application/x-tar", null, PICK_BACKUP, StorageUtil.getDownloadUri());
this,
"application/x-tar",
null,
PICK_BACKUP,
StorageUtil.getDownloadUri(),
false);
} else {
final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath());
if (backupFile != null) {
@@ -4,15 +4,10 @@ import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
public abstract class Attachment {
@@ -116,14 +111,7 @@ public abstract class Attachment {
filename = filename.substring(0, i);
}
}
String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext);
// copy content to this file
InputStream inputStream = PartAuthority.getAttachmentStream(context, getDataUri());
OutputStream outputStream = new FileOutputStream(path);
Util.copy(inputStream, outputStream);
return path;
return DcHelper.copyToBlobdir(context, getDataUri(), filename, ext);
} catch (Exception e) {
e.printStackTrace();
return null;
@@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.calls;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationManagerCompat;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.R;
@RequiresApi(api = Build.VERSION_CODES.O)
public class CallActionReceiver extends BroadcastReceiver {
private static final String TAG = "CallActionReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) return;
String action = intent.getAction();
Log.d(TAG, "Received action: " + action);
if (CallActivity.ACTION_DECLINE_CALL.equals(action)) {
CallCoordinator.getInstance(context).declineCall();
} else if (CallActivity.ACTION_HANGUP_CALL.equals(action)) {
CallCoordinator.getInstance(context).hangUp();
} else if (CallActivity.ACTION_CALL_BACK.equals(action)) {
int chatId = intent.getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
int accId = intent.getIntExtra(ConversationActivity.ACCOUNT_ID_EXTRA, -1);
boolean video = intent.getBooleanExtra(CallActivity.EXTRA_STARTS_WITH_VIDEO, false);
if (chatId > 0 && accId > 0) {
NotificationManagerCompat.from(context).cancel(CallCoordinator.NOTIFICATION_ID_MISSED_CALL);
CallCoordinator coordinator = CallCoordinator.getInstance(context);
if (coordinator.hasActiveCall()) {
Toast.makeText(context, R.string.already_in_call, Toast.LENGTH_SHORT).show();
} else {
coordinator.initiateOutgoingCall(accId, chatId, video);
}
}
} else if (CallActivity.ACTION_MESSAGE.equals(action)) {
int chatId = intent.getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
int accId = intent.getIntExtra(ConversationActivity.ACCOUNT_ID_EXTRA, -1);
NotificationManagerCompat.from(context).cancel(CallCoordinator.NOTIFICATION_ID_MISSED_CALL);
if (chatId > 0 && accId > 0) {
Intent convIntent = new Intent(context, ConversationActivity.class);
convIntent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
convIntent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
convIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(convIntent);
}
}
}
}
@@ -63,6 +63,9 @@ public class CallActivity extends AppCompatActivity {
public static final String ACTION_ANSWER_CALL = BuildConfig.APPLICATION_ID + ".ANSWER_CALL";
public static final String ACTION_DECLINE_CALL = BuildConfig.APPLICATION_ID + ".DECLINE_CALL";
public static final String ACTION_HANGUP_CALL = BuildConfig.APPLICATION_ID + ".HANGUP_CALL";
public static final String ACTION_CALL_BACK = BuildConfig.APPLICATION_ID + ".CALL_BACK";
public static final String ACTION_MESSAGE = BuildConfig.APPLICATION_ID + ".MESSAGE";
public static final String EXTRA_STARTS_WITH_VIDEO = "starts_with_video";
// Views
@@ -57,6 +57,7 @@ import kotlinx.coroutines.Dispatchers;
import kotlinx.coroutines.flow.Flow;
import kotlinx.coroutines.flow.FlowKt;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -70,7 +71,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
// Notification channels
private static final String CHANNEL_ID_INCOMING = "voip_incoming_calls";
private static final String CHANNEL_ID_ONGOING = "voip_ongoing_calls";
private static final String CHANNEL_ID_MISSED = "voip_missed_calls";
private static final int NOTIFICATION_ID_CALL = 1001;
static final int NOTIFICATION_ID_MISSED_CALL = 1002;
private static final int PI_ANSWER = 0;
private static final int PI_DECLINE = 1;
private static final int PI_FULLSCREEN = 2;
private static final int PI_HANGUP = 3;
private static final int PI_ONGOING_CONTENT = 4;
private static final int PI_MISSED_CONTENT = 5;
private static final int PI_MISSED_CALLBACK = 6;
private static final int PI_MISSED_MESSAGE = 7;
private static final String CALL_IDENTIFIER_SCHEME = "deltachat:";
@@ -123,6 +135,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
private boolean hasNotifiedBackend = false;
private boolean hasAutoSelectedEarpiece = false;
private boolean pendingMediaCapture = false;
private boolean wasAnsweredLocally = false;
private CallControlScope activeCallControlScope;
private CallViewModel activeCallViewModel;
@@ -169,8 +182,14 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
ongoingChannel.setDescription("Notifications for active DeltaChat calls");
ongoingChannel.setSound(null, null);
NotificationChannel missedChannel =
new NotificationChannel(
CHANNEL_ID_MISSED, "Missed Calls", NotificationManager.IMPORTANCE_HIGH);
missedChannel.setDescription("Notifications for missed DeltaChat calls");
notificationManager.createNotificationChannel(incomingChannel);
notificationManager.createNotificationChannel(ongoingChannel);
notificationManager.createNotificationChannel(missedChannel);
}
private void registerTelecom() {
@@ -184,6 +203,29 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
}
}
private void disconnectTelecom(DisconnectCause cause) {
CallControlScope scope = activeCallControlScope;
if (scope == null) {
Log.d(TAG, "No active CallControlScope, skipping disconnect");
return;
}
scope.disconnect(
cause,
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
Log.d(TAG, "Telecom disconnect completed: " + result);
}
});
}
private void addEventListeners() {
DcEventCenter eventCenter = DcHelper.getEventCenter(this.appContext);
eventCenter.removeObservers(this);
@@ -418,6 +460,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return;
}
wasAnsweredLocally = true;
if (callService != null) {
callService.stopRingtone();
}
@@ -581,28 +625,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
notifyBackendCallEnded();
// Disconnect with CallControlScope
CallControlScope scope = activeCallControlScope;
if (scope != null) {
scope.disconnect(
new DisconnectCause(DisconnectCause.REJECTED),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
if (result instanceof CallControlResult) {
Log.d(TAG, "Decline succeeded with CallControlScope");
} else if (result instanceof kotlin.Result.Failure) {
Log.e(TAG, "Decline failed", ((kotlin.Result.Failure) result).exception);
}
}
});
}
disconnectTelecom(new DisconnectCause(DisconnectCause.REJECTED));
// End call on service
if (callService != null) {
@@ -623,28 +646,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
notifyBackendCallEnded();
// Disconnect with CallControlScope
CallControlScope scope = activeCallControlScope;
if (scope != null) {
scope.disconnect(
new DisconnectCause(DisconnectCause.LOCAL),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
if (result instanceof CallControlResult) {
Log.d(TAG, "Hang up succeeded with CallControlScope");
} else if (result instanceof kotlin.Result.Failure) {
Log.e(TAG, "Hang up failed", ((kotlin.Result.Failure) result).exception);
}
}
});
}
disconnectTelecom(new DisconnectCause(DisconnectCause.LOCAL));
// End call on service
if (callService != null) {
@@ -890,7 +892,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
// This event is problematic because it can trigger in both directions,
// in addition to multiple other scenarios which cannot easily be distinguished
// May cause problems in edge cases
onCallEnded(accId, callId);
onCallEnded(accId, callId, startsWithVideo);
break;
}
});
@@ -991,24 +993,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
answeredElsewhere.postValue(true);
// Disconnect from Telecom CallControlScope
CallControlScope scope = activeCallControlScope;
if (scope != null) {
scope.disconnect(
new DisconnectCause(DisconnectCause.REMOTE),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
Log.d(TAG, "Disconnect (answered elsewhere) completed");
}
});
}
disconnectTelecom(new DisconnectCause(DisconnectCause.REMOTE));
if (callService != null) {
callService.endCall();
@@ -1058,7 +1043,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
showOrUpdateOngoingNotification(appContext.getString(R.string.call_with, calleeName));
}
private synchronized void onCallEnded(int accId, int callId) {
private synchronized void onCallEnded(int accId, int callId, boolean startsWithVideo) {
Log.d(TAG, "onCallEnded: accId=" + accId + ", callId=" + callId);
if (!hasActiveCall()) {
@@ -1086,30 +1071,16 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
callService.stopRingtone();
}
// Disconnect from CallControlScope
if (activeCallControlScope != null) {
activeCallControlScope.disconnect(
// We actually don't know if this is incoming or outgoing
// But we have to provide one of LOCAL, REMOTE, MISSED, REJECTED
new DisconnectCause(DisconnectCause.REMOTE),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
Log.d(TAG, "Disconnect completed");
}
});
}
disconnectTelecom(new DisconnectCause(DisconnectCause.REMOTE));
if (callService != null) {
callService.endCall();
}
if (isIncomingCall && !wasAnsweredLocally) {
showMissedCallNotification(activeAccId, activeChatId, startsWithVideo);
}
// Clear active states
cleanupCall(accId, callId);
}
@@ -1128,6 +1099,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
notifyBackendCallEnded();
DisconnectCause cause;
if (state == PeerConnection.PeerConnectionState.FAILED) {
cause = new DisconnectCause(DisconnectCause.REMOTE, "PeerConnection failed");
} else {
cause = new DisconnectCause(DisconnectCause.LOCAL, "PeerConnection closed");
}
disconnectTelecom(cause);
if (callService != null) {
callService.endCall();
}
// Cleanup
if (hasActiveCall()) {
cleanupCall(activeAccId, activeCallId);
@@ -1169,6 +1152,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
this.hasNotifiedBackend = false;
this.hasAutoSelectedEarpiece = false;
this.pendingMediaCapture = false;
this.wasAnsweredLocally = false;
mainHandler.removeCallbacks(outgoingRingtoneRunnable);
@@ -1459,19 +1443,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
PendingIntent answerPendingIntent =
PendingIntent.getActivity(
this.appContext,
0,
PI_ANSWER,
answerIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// Decline intent
Intent declineIntent = new Intent(this.appContext, CallActivity.class);
Intent declineIntent = new Intent(this.appContext, CallActionReceiver.class);
declineIntent.setAction(CallActivity.ACTION_DECLINE_CALL);
declineIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent declinePendingIntent =
PendingIntent.getActivity(
PendingIntent.getBroadcast(
this.appContext,
1,
PI_DECLINE,
declineIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
@@ -1482,7 +1465,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
PendingIntent fullScreenPendingIntent =
PendingIntent.getActivity(
this.appContext,
2,
PI_FULLSCREEN,
fullScreenIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
@@ -1533,26 +1516,106 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
notificationManager.notify(NOTIFICATION_ID_CALL, builder.build());
}
private void showMissedCallNotification(int accId, int chatId, boolean wasVideoCall) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (!hasNotificationPermission()) {
Log.w(TAG, "Cannot show missed call notification: no permission");
return;
}
}
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accId);
DcChat dcChat = dcContext.getChat(chatId);
String callerName = CallUtil.getNameFromChat(dcChat);
Intent contentAction = new Intent(appContext, ConversationActivity.class);
contentAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
contentAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
contentAction.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent contentIntent =
PendingIntent.getActivity(
appContext,
PI_MISSED_CONTENT,
contentAction,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Intent callBackAction = new Intent(appContext, CallActionReceiver.class);
callBackAction.setAction(CallActivity.ACTION_CALL_BACK);
callBackAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
callBackAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
callBackAction.putExtra(CallActivity.EXTRA_STARTS_WITH_VIDEO, wasVideoCall);
PendingIntent callBackIntent =
PendingIntent.getBroadcast(
appContext,
PI_MISSED_CALLBACK,
callBackAction,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Intent messageAction = new Intent(appContext, CallActionReceiver.class);
messageAction.setAction(CallActivity.ACTION_MESSAGE);
messageAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
messageAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
PendingIntent messageIntent =
PendingIntent.getBroadcast(
appContext,
PI_MISSED_MESSAGE,
messageAction,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
String contentText = appContext.getString(R.string.missed_call);
Notification.Builder builder =
new Notification.Builder(appContext, CHANNEL_ID_MISSED)
.setSmallIcon(R.drawable.icon_notification)
.setContentTitle(callerName)
.setContentText(contentText)
.setContentIntent(contentIntent)
.setAutoCancel(true)
.addAction(
new Notification.Action.Builder(
null, appContext.getString(R.string.call_back), callBackIntent)
.build())
.addAction(
new Notification.Action.Builder(
null, appContext.getString(R.string.chat_input_placeholder), messageIntent)
.build());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setCategory(Notification.CATEGORY_MISSED_CALL);
} else {
builder.setCategory(Notification.CATEGORY_CALL);
}
Icon icon = displayIcon.getValue();
if (icon != null) {
builder.setLargeIcon(icon);
}
notificationManager.notify(NOTIFICATION_ID_MISSED_CALL, builder.build());
}
private Notification buildOngoingCallNotification(
String statusText, String displayName, Icon icon) {
Intent activityIntent = new Intent(this.appContext, CallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
Intent hangupIntent = new Intent(this.appContext, CallActivity.class);
Intent hangupIntent = new Intent(this.appContext, CallActionReceiver.class);
hangupIntent.setAction(CallActivity.ACTION_HANGUP_CALL);
hangupIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent hangupPendingIntent =
PendingIntent.getActivity(
PendingIntent.getBroadcast(
this.appContext,
3,
PI_HANGUP,
hangupIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
PendingIntent contentIntent =
PendingIntent.getActivity(
this.appContext,
4,
PI_ONGOING_CONTENT,
activityIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
@@ -9,10 +9,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import java.util.List;
import org.thoughtcrime.securesms.EglUtils;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerationAndroid;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
@@ -22,6 +24,7 @@ import org.webrtc.VideoCapturer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
@RequiresApi(Build.VERSION_CODES.M)
public class MediaStreamManager {
private static final String TAG = "MediaStreamManager";
@@ -29,9 +32,9 @@ public class MediaStreamManager {
private static final String AUDIO_TRACK_ID = "audio_track";
private static final String VIDEO_TRACK_ID = "video_track";
private static final int VIDEO_WIDTH = 1280;
private static final int VIDEO_HEIGHT = 720;
private static final int VIDEO_FPS = 30;
private static final int TARGET_WIDTH = 1280;
private static final int TARGET_HEIGHT = 720;
private static final int TARGET_FPS = 30;
private final Context context;
private final PeerConnectionFactory peerConnectionFactory;
@@ -42,6 +45,9 @@ public class MediaStreamManager {
private SurfaceTextureHelper surfaceTextureHelper;
private volatile boolean isFrontCamera = true;
private volatile boolean isCapturing = false;
private volatile String currentDeviceName;
private volatile int currentCaptureWidth;
private volatile int currentCaptureHeight;
public interface Callback {
void onMediaStreamReady(MediaStream stream);
@@ -90,7 +96,6 @@ public class MediaStreamManager {
*
* @return true if the camera is capturing, false if it could not be started
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public synchronized boolean startVideoCapture() {
if (isCapturing) {
return true;
@@ -116,9 +121,23 @@ public class MediaStreamManager {
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
}
videoCapturer.startCapture(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS);
int[] captureFormat = selectCaptureFormat(currentDeviceName);
currentCaptureWidth = captureFormat[0];
currentCaptureHeight = captureFormat[1];
videoCapturer.startCapture(currentCaptureWidth, currentCaptureHeight, TARGET_FPS);
videoSource.adaptOutputFormat(TARGET_WIDTH, TARGET_HEIGHT, TARGET_FPS);
isCapturing = true;
Log.d(TAG, "Video capture started");
Log.d(
TAG,
"Video capture started at "
+ currentCaptureWidth
+ "x"
+ currentCaptureHeight
+ ", adapted to "
+ TARGET_WIDTH
+ "x"
+ TARGET_HEIGHT);
return true;
}
@@ -141,6 +160,60 @@ public class MediaStreamManager {
isCapturing = false;
}
private int[] selectCaptureFormat(@Nullable String deviceName) {
if (deviceName == null) {
Log.w(TAG, "Device name is null, using target dimensions");
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
}
Camera2Enumerator enumerator = new Camera2Enumerator(context);
List<CameraEnumerationAndroid.CaptureFormat> formats =
enumerator.getSupportedFormats(deviceName);
if (formats == null || formats.isEmpty()) {
Log.w(TAG, "No supported formats for " + deviceName);
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
}
CameraEnumerationAndroid.CaptureFormat best = null;
int bestPixels = Integer.MAX_VALUE;
for (CameraEnumerationAndroid.CaptureFormat f : formats) {
if (f.width >= TARGET_WIDTH && f.height >= TARGET_HEIGHT) {
int pixels = f.width * f.height;
if (pixels < bestPixels) {
bestPixels = pixels;
best = f;
}
}
}
if (best != null) {
Log.d(
TAG, "Selected capture format: " + best.width + "x" + best.height + " for " + deviceName);
return new int[] {best.width, best.height};
}
CameraEnumerationAndroid.CaptureFormat largest = null;
int largestPixels = 0;
for (CameraEnumerationAndroid.CaptureFormat f : formats) {
int pixels = f.width * f.height;
if (pixels > largestPixels) {
largestPixels = pixels;
largest = f;
}
}
if (largest != null) {
Log.w(
TAG,
"Using largest format " + largest.width + "x" + largest.height + " for " + deviceName);
return new int[] {largest.width, largest.height};
}
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
}
@Nullable
private VideoCapturer createVideoCapturer() {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
@@ -158,6 +231,7 @@ public class MediaStreamManager {
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer != null) {
isFrontCamera = true;
currentDeviceName = deviceName;
return capturer;
}
}
@@ -168,6 +242,7 @@ public class MediaStreamManager {
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer != null) {
isFrontCamera = enumerator.isFrontFacing(deviceName);
currentDeviceName = deviceName;
return capturer;
}
}
@@ -224,6 +299,30 @@ public class MediaStreamManager {
public void onCameraSwitchDone(boolean isFront) {
Log.d(TAG, "switchCamera SUCCESS, isFront=" + isFront);
isFrontCamera = isFront;
currentDeviceName = finalTargetCameraName;
int[] newFormat = selectCaptureFormat(finalTargetCameraName);
if (newFormat[0] != currentCaptureWidth || newFormat[1] != currentCaptureHeight) {
Log.d(
TAG,
"Changing capture format: "
+ currentCaptureWidth
+ "x"
+ currentCaptureHeight
+ " to "
+ newFormat[0]
+ "x"
+ newFormat[1]);
currentCaptureWidth = newFormat[0];
currentCaptureHeight = newFormat[1];
cameraVideoCapturer.changeCaptureFormat(
currentCaptureWidth, currentCaptureHeight, TARGET_FPS);
}
if (videoSource != null) {
videoSource.adaptOutputFormat(TARGET_WIDTH, TARGET_HEIGHT, TARGET_FPS);
}
if (callback != null) callback.onCameraSwitch(isFront);
}
@@ -360,6 +360,31 @@ public class InputPanel extends ConstraintLayout
}
}
public boolean isRecording() {
return microphoneRecorderView.isRecording();
}
public long getRecordingDuration() {
return recordTime.getElapsedTime();
}
public void resetRecordingUI() {
microphoneRecorderView.resetState();
recordLockCancel.setVisibility(View.GONE);
recordTime.hide();
slideToCancel.hide();
emojiToggle.setVisibility(View.VISIBLE);
emojiToggle.setAlpha(1f);
composeText.setVisibility(View.VISIBLE);
composeText.setAlpha(1f);
quickCameraToggle.setVisibility(View.VISIBLE);
quickCameraToggle.setAlpha(1f);
quickAudioToggle.setVisibility(View.VISIBLE);
quickAudioToggle.setAlpha(1f);
buttonToggle.setAlpha(1f);
}
public interface Listener {
void onRecorderStarted();
@@ -452,10 +477,10 @@ public class InputPanel extends ConstraintLayout
}
public long hide() {
long elapsedtime = System.currentTimeMillis() - startTime.get();
long elapsedTime = getElapsedTime();
this.startTime.set(0);
ViewUtil.fadeOut(this.recordTimeView, FADE_TIME, View.INVISIBLE);
return elapsedtime;
return elapsedTime;
}
@Override
@@ -468,6 +493,11 @@ public class InputPanel extends ConstraintLayout
}
}
public long getElapsedTime() {
long start = startTime.get();
return start > 0 ? System.currentTimeMillis() - start : 0;
}
private String formatElapsedTime(long ms) {
return DateUtils.formatElapsedTime(TimeUnit.MILLISECONDS.toSeconds(ms))
+ String.format(".%01d", ((ms / 100) % 10));
@@ -53,10 +53,11 @@ public class MediaView extends FrameLayout {
@NonNull Window window,
@NonNull Uri source,
@Nullable String fileName,
@NonNull String mediaType,
@Nullable String mediaType,
long size,
boolean autoplay)
throws IOException {
mediaType = mediaType == null ? "null" : mediaType;
if (mediaType.startsWith("image/")) {
imageView.setVisibility(View.VISIBLE);
if (videoView.resolved()) videoView.get().setVisibility(View.GONE);
@@ -287,4 +287,15 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
.start();
}
}
public boolean isRecording() {
return state != State.NOT_RUNNING;
}
public void resetState() {
if (state != State.NOT_RUNNING) {
state = State.NOT_RUNNING;
hideUi();
}
}
}
@@ -27,13 +27,11 @@ import java.util.concurrent.Executors;
public class AudioPlaybackViewModel extends ViewModel {
private static final String TAG = "AudioPlaybackViewModel";
private static final int NON_MESSAGE_AUDIO_MSG_ID =
0; // Audios not attached to a message doesn't have message id.
private final MutableLiveData<AudioPlaybackState> playbackState;
private final MutableLiveData<Map<Integer, Long>> durations =
new MutableLiveData<>(new HashMap<>());
private final Map<Integer, Uri> durationUris = new HashMap<>();
private final Set<Integer> extractionInProgress = new HashSet<>();
private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2);
@@ -115,7 +113,17 @@ public class AudioPlaybackViewModel extends ViewModel {
// Check cache
Map<Integer, Long> currentDurations = durations.getValue();
if (currentDurations != null && currentDurations.containsKey(msgId)) {
return;
Uri cachedUri = durationUris.get(msgId);
if (audioUri.equals(cachedUri)) {
return;
}
Map<Integer, Long> updated = new HashMap<>(currentDurations);
updated.remove(msgId);
durations.setValue(updated);
durationUris.remove(msgId);
synchronized (extractionInProgress) {
extractionInProgress.remove(msgId);
}
}
// Check extracting
@@ -136,6 +144,7 @@ public class AudioPlaybackViewModel extends ViewModel {
Map<Integer, Long> updatedDurations = new HashMap<>(durations.getValue());
updatedDurations.put(msgId, duration);
durations.setValue(updatedDurations);
durationUris.put(msgId, audioUri);
});
synchronized (extractionInProgress) {
@@ -193,10 +202,6 @@ public class AudioPlaybackViewModel extends ViewModel {
return current != null && String.valueOf(msgId).equals(current.mediaId);
}
public void stopNonMessageAudioPlayback() {
stopByIds(NON_MESSAGE_AUDIO_MSG_ID);
}
// A special method for deleting message, where we only use message Ids
public void stopByIds(int... msgIds) {
if (mediaController == null) return;
@@ -259,7 +264,10 @@ public class AudioPlaybackViewModel extends ViewModel {
updateCurrentState(false);
} else if (player.getPlaybackState() == Player.STATE_ENDED
&& !player.hasNextMediaItem()) {
mediaController.setPlayWhenReady(false);
mediaController.stop();
mediaController.clearMediaItems();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
}
@@ -386,6 +394,7 @@ public class AudioPlaybackViewModel extends ViewModel {
protected void onCleared() {
stopUpdateProgress();
extractionExecutor.shutdown();
durationUris.clear();
super.onCleared();
}
}
@@ -109,7 +109,10 @@ public class AudioView extends FrameLayout {
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
if (state != null && msgId == state.getMsgId()) {
if (state != null
&& msgId == state.getMsgId()
&& (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING
|| state.getStatus() == AudioPlaybackState.PlaybackStatus.PAUSED)) {
// Same audio
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
viewModel.pause(msgId);
@@ -194,14 +197,17 @@ public class AudioView extends FrameLayout {
seekBar.setEnabled(true);
this.progress = 0;
this.duration = 0;
viewModel.ensureDurationLoaded(getContext(), msgId, audioUri);
// Get duration
Map<Integer, Long> durations = viewModel.getDurations().getValue();
if (durations != null && durations.containsKey(msgId)) {
this.duration = Math.toIntExact(durations.get(msgId));
updateTimestampsAndSeekBar();
} else {
viewModel.ensureDurationLoaded(getContext(), msgId, audioUri);
}
updateTimestampsAndSeekBar();
if (audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
title.setVisibility(View.GONE);
@@ -340,16 +346,18 @@ public class AudioView extends FrameLayout {
private void onDurationsChanged(Map<Integer, Long> durations) {
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
// When there is no playback happening, msgId can be -1
if (state != null && msgId >= 0 && msgId == state.getMsgId()) {
if (state != null
&& msgId >= 0
&& msgId == state.getMsgId()
&& (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING
|| state.getStatus() == AudioPlaybackState.PlaybackStatus.PAUSED)) {
return;
}
Long duration = durations.get(msgId);
if (duration != null && seekBar.getMax() <= 100) {
if (duration != null) {
this.duration = Math.toIntExact(duration);
updateTimestampsAndSeekBar();
seekBar.setMax(this.duration);
}
}
@@ -46,7 +46,7 @@ public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
if (query == null && addScanQRLink) {
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_QR_INVITE);
}
if (addCreateContactLink && !dcContext.isChatmail()) {
if (addCreateContactLink && dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
additional_items =
Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT);
}
@@ -54,7 +54,7 @@ public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_GROUP);
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST);
if (!dcContext.isChatmail()) {
if (dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
additional_items =
Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP);
}
@@ -23,6 +23,10 @@ import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import com.b44t.messenger.DcMsg;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.HashMap;
import org.thoughtcrime.securesms.ApplicationContext;
@@ -31,6 +35,7 @@ import org.thoughtcrime.securesms.LocalHelpActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.notifications.NotificationCenter;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.qr.QrActivity;
@@ -441,6 +446,15 @@ public class DcHelper {
return getBlobdirFile(dcContext, filename, ext);
}
public static String copyToBlobdir(Context context, Uri uri, String filename, String ext)
throws IOException {
String path = getBlobdirFile(getContext(context), filename, ext);
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
OutputStream outputStream = new FileOutputStream(path);
Util.copy(inputStream, outputStream);
return path;
}
@NonNull
public static ThreadRecord getThreadRecord(
Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
@@ -514,16 +528,20 @@ public class DcHelper {
.show();
}
public static void showInvalidUnencryptedDialog(Context context) {
new AlertDialog.Builder(context)
public static AlertDialog.Builder prepareInvalidUnencryptedDialog(
Context context, AlertDialog.Builder builder) {
return builder
.setMessage(context.getString(R.string.invalid_unencrypted_explanation))
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#howtoe2ee"))
.setNegativeButton(
R.string.qrscan_title,
(d, w) -> context.startActivity(new Intent(context, QrActivity.class)))
.setPositiveButton(R.string.ok, null)
.setCancelable(true)
.show();
.setCancelable(true);
}
public static void showInvalidUnencryptedDialog(Context context) {
prepareInvalidUnencryptedDialog(context, new AlertDialog.Builder(context)).show();
}
public static void openHelp(Context context, String section) {
@@ -1,53 +0,0 @@
package org.thoughtcrime.securesms.geolocation;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.HashSet;
import java.util.Set;
public final class ActiveLocationChats {
private static final String PREFS_NAME = "location_streaming";
private static final String KEY_ACTIVE = "active_chat_ids";
private ActiveLocationChats() {}
private static SharedPreferences prefs(Context context) {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
/**
* Add a chat. Uses commit() to guarantee the write reaches disk before the process can die to
* preserve the superset invariant.
*/
static void add(Context context, int chatId) {
Set<String> current = new HashSet<>(getAll(context));
current.add(String.valueOf(chatId));
prefs(context).edit().putStringSet(KEY_ACTIVE, current).commit();
}
public static void remove(Context context, int chatId) {
Set<String> current = new HashSet<>(getAll(context));
current.remove(String.valueOf(chatId));
prefs(context).edit().putStringSet(KEY_ACTIVE, current).apply();
}
static void clear(Context context) {
prefs(context).edit().remove(KEY_ACTIVE).apply();
}
static Set<Integer> getAllIds(Context context) {
Set<Integer> ids = new HashSet<>();
for (String s : getAll(context)) {
try {
ids.add(Integer.parseInt(s));
} catch (NumberFormatException ignored) {
}
}
return ids;
}
private static Set<String> getAll(Context context) {
return prefs(context).getStringSet(KEY_ACTIVE, new HashSet<>());
}
}
@@ -17,13 +17,15 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.content.ContextCompat;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
public class LocationStreamingService extends Service {
private static final String TAG = "LocationStreamingService";
private static final String TAG = "LocationStreamingSvc";
private static final String ACTION_STOP = "org.thoughtcrime.securesms.geolocation.STOP_STREAMING";
private static final int NOTIFICATION_ID = 8801;
private static final String CHANNEL_ID = "location_streaming";
@@ -36,38 +38,63 @@ public class LocationStreamingService extends Service {
// static API
/** Register a chat for location updates, then ensure the service is running. */
public static void startSharing(Context context, int chatId, int durationSeconds) {
ActiveLocationChats.add(context, chatId);
DcHelper.getContext(context).sendLocationsToChat(chatId, durationSeconds);
ContextCompat.startForegroundService(
context, new Intent(context, LocationStreamingService.class));
}
/** Unregister a chat. If no chats remain, stop the service. */
public static void stopSharing(Context context, int chatId) {
ActiveLocationChats.remove(context, chatId);
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
if (!DcHelper.getContext(context).isSendingLocationsToChat(0)) {
context.stopService(new Intent(context, LocationStreamingService.class));
}
}
public static void ensureRunning(Context context) {
if (!hasLocationPermission(context)) {
for (int chatId : ActiveLocationChats.getAllIds(context)) {
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
}
ActiveLocationChats.clear(context);
public static void startSharing(Context context, int accountId, int chatId, int durationSeconds) {
try {
DcHelper.getRpc(context).sendLocationsToChat(accountId, chatId, durationSeconds);
} catch (RpcException e) {
Log.e(TAG, "Failed to start location streaming", e);
return;
}
ContextCompat.startForegroundService(
context, new Intent(context, LocationStreamingService.class));
}
/** Unregister a chat. If no chats remain, stop the service. */
public static void stopSharing(Context context, int accountId, int chatId) {
try {
DcHelper.getRpc(context).sendLocationsToChat(accountId, chatId, 0);
} catch (RpcException e) {
Log.e(TAG, "Failed to stop location streaming", e);
}
if (!isAnySharingActive(context)) {
context.stopService(new Intent(context, LocationStreamingService.class));
}
}
public static void ensureRunning(Context context) {
if (!hasLocationPermission(context)) {
try {
DcHelper.getRpc(context).stopSendingLocations();
} catch (RpcException e) {
Log.e(TAG, "Failed to stop location streaming", e);
}
return;
}
if (isAnySharingActive(context)) {
ContextCompat.startForegroundService(
context, new Intent(context, LocationStreamingService.class));
}
}
public static boolean isRunning() {
return running;
}
private static boolean isAnySharingActive(Context context) {
try {
Rpc rpc = DcHelper.getRpc(context);
for (int accountId : rpc.getAllAccountIds()) {
if (rpc.isSendingLocations(accountId)) {
return true;
}
}
} catch (RpcException e) {
Log.e(TAG, "Failed to check location streaming state", e);
return true;
}
return false;
}
// lifecycle
@Override
@@ -86,7 +113,7 @@ public class LocationStreamingService extends Service {
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
stopAllSharing();
stopAllSharing(this);
stopSelf();
return START_NOT_STICKY;
}
@@ -100,11 +127,12 @@ public class LocationStreamingService extends Service {
return START_STICKY;
}
private void stopAllSharing() {
for (int chatId : ActiveLocationChats.getAllIds(this)) {
DcHelper.getContext(this).sendLocationsToChat(chatId, 0);
private static void stopAllSharing(Context context) {
try {
DcHelper.getRpc(context).stopSendingLocations();
} catch (RpcException e) {
Log.e(TAG, "Failed to stop sending locations", e);
}
ActiveLocationChats.clear(this);
}
@Nullable
@@ -150,16 +178,21 @@ public class LocationStreamingService extends Service {
private void publishAndWrite(Location location) {
LocationData.getInstance().post(location);
boolean keepGoing =
DcHelper.getContext(this)
.setLocation(
(float) location.getLatitude(),
(float) location.getLongitude(),
location.getAccuracy());
boolean keepGoing;
try {
keepGoing =
DcHelper.getRpc(this)
.setLocation(
(float) location.getLatitude(),
(float) location.getLongitude(),
location.getAccuracy());
} catch (RpcException e) {
Log.e(TAG, "Failed to set location", e);
return;
}
Log.d(TAG, "keepGoing: " + keepGoing);
if (!keepGoing) {
stopAllSharing();
stopSelf();
}
}
@@ -17,14 +17,12 @@
package org.thoughtcrime.securesms.mms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
@@ -36,6 +34,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.util.ListenableFuture;
import chat.delta.util.SettableFuture;
@@ -63,22 +62,21 @@ import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.geolocation.ActiveLocationChats;
import org.thoughtcrime.securesms.geolocation.LocationStreamingService;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.guava.Optional;
import org.thoughtcrime.securesms.util.views.Stub;
public class AttachmentManager {
private static final String TAG = "AttachmentManager";
private final @NonNull Context context;
private final @NonNull Stub<View> attachmentViewStub;
private final @NonNull View attachmentView;
private final @NonNull AttachmentListener attachmentListener;
private RemovableEditableMediaView removableMediaView;
@@ -99,20 +97,16 @@ public class AttachmentManager {
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
this.context = activity;
this.attachmentListener = listener;
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
}
private void inflateStub() {
if (!attachmentViewStub.resolved()) {
View root = attachmentViewStub.get();
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc);
this.vcardView = ViewUtil.findById(root, R.id.attachment_vcard);
// this.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
this.attachmentView = ViewUtil.findById(activity, R.id.attachment_editor);
if (this.attachmentView != null) {
this.thumbnail = ViewUtil.findById(attachmentView, R.id.attachment_thumbnail);
this.audioView = ViewUtil.findById(attachmentView, R.id.attachment_audio);
this.documentView = ViewUtil.findById(attachmentView, R.id.attachment_document);
this.webxdcView = ViewUtil.findById(attachmentView, R.id.attachment_webxdc);
this.vcardView = ViewUtil.findById(attachmentView, R.id.attachment_vcard);
// this.mapView = ViewUtil.findById(attachmentView, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(attachmentView, R.id.removable_media_view);
removableMediaView.addRemoveClickListener(new RemoveButtonListener());
removableMediaView.setEditClickListener(new EditButtonListener());
@@ -121,10 +115,10 @@ public class AttachmentManager {
}
public void clear(@NonNull GlideRequests glideRequests, boolean animate) {
if (attachmentViewStub.resolved()) {
if (this.attachmentView != null) {
if (animate) {
ViewUtil.fadeOut(attachmentViewStub.get(), 200)
ViewUtil.fadeOut(attachmentView, 200)
.addListener(
new ListenableFuture.Listener<Boolean>() {
@Override
@@ -221,7 +215,6 @@ public class AttachmentManager {
}
*/
@SuppressLint("StaticFieldLeak")
public ListenableFuture<Boolean> setMedia(
@NonNull final GlideRequests glideRequests,
@NonNull final Uri uri,
@@ -231,149 +224,104 @@ public class AttachmentManager {
final int height,
final int chatId,
AudioPlaybackViewModel playbackViewModel) {
inflateStub();
final SettableFuture<Boolean> result = new SettableFuture<>();
new AsyncTask<Void, Void, Slide>() {
@Override
protected void onPreExecute() {
thumbnail.clear(glideRequests);
setAttachmentPresent(true);
}
thumbnail.clear(glideRequests);
setAttachmentPresent(true);
@Override
protected @Nullable Slide doInBackground(Void... params) {
try {
if (msg != null && msg.getType() == DcMsg.DC_MSG_WEBXDC) {
return new DocumentSlide(context, msg);
} else if (PartAuthority.isLocalUri(uri)) {
return getManuallyCalculatedSlideInfo(uri, width, height, msg);
} else {
Slide result = getContentResolverSlideInfo(uri, width, height, chatId);
if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height, msg);
else return result;
}
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(@Nullable final Slide slide) {
if (slide == null) {
setAttachmentPresent(false);
result.set(false);
} else if (slide.getFileSize() > 1024 * 1024 * 1024) {
// this is only a rough check, videos and images may be recoded
// and the core checks more carefully later.
setAttachmentPresent(false);
Log.w(TAG, "File too large.");
Toast.makeText(slide.context, "File too large.", Toast.LENGTH_LONG).show();
result.set(false);
} else {
setSlide(slide);
setAttachmentPresent(true);
if (slide.hasAudio()) {
audioView.setPlaybackViewModel(playbackViewModel);
audioView.setAudio((AudioSlide) slide);
removableMediaView.display(audioView, false);
removableMediaView.addRemoveClickListener(
v -> {
playbackViewModel.stop(audioView.getMsgId());
});
result.set(true);
} else if (slide.isVcard()) {
vcardView.setVcard(glideRequests, (VcardSlide) slide, DcHelper.getRpc(context));
removableMediaView.display(vcardView, false);
} else if (slide.hasDocument()) {
if (slide.isWebxdcDocument()) {
DcMsg instance =
msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId);
webxdcView.setWebxdc(instance, context.getString(R.string.webxdc_draft_hint));
webxdcView.setWebxdcClickListener(
(v, s) -> {
WebxdcActivity.openWebxdcActivity(context, instance);
});
removableMediaView.display(webxdcView, false);
Util.runOnBackground(
() -> {
Slide slide = null;
try {
if (msg != null && msg.getType() == DcMsg.DC_MSG_WEBXDC) {
slide = new DocumentSlide(context, msg);
} else if (msg != null
&& (msg.getType() == DcMsg.DC_MSG_AUDIO || msg.getType() == DcMsg.DC_MSG_VOICE)) {
slide = new AudioSlide(context, msg);
} else if (PartAuthority.isLocalUri(uri)) {
slide = getManuallyCalculatedSlideInfo(uri, width, height, msg, mediaType, chatId);
} else {
documentView.setDocument((DocumentSlide) slide);
removableMediaView.display(documentView, false);
slide = getContentResolverSlideInfo(uri, width, height, chatId, mediaType);
if (slide == null) {
slide = getManuallyCalculatedSlideInfo(uri, width, height, msg, mediaType, chatId);
}
}
result.set(true);
} else {
Attachment attachment = slide.asAttachment();
result.deferTo(
thumbnail.setImageResource(
glideRequests, slide, attachment.getWidth(), attachment.getHeight()));
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
} catch (IOException e) {
Log.w(TAG, e);
}
attachmentListener.onAttachmentChanged();
}
}
final Slide finalSlide = slide;
Util.runOnMain(
() -> {
if (finalSlide == null) {
setAttachmentPresent(false);
result.set(false);
} else if (finalSlide.getFileSize() > 1024 * 1024 * 1024) {
// this is only a rough check, videos and images may be recoded
// and the core checks more carefully later.
setAttachmentPresent(false);
Log.w(TAG, "File too large.");
Toast.makeText(finalSlide.context, "File too large.", Toast.LENGTH_LONG).show();
result.set(false);
} else {
setSlide(finalSlide);
setAttachmentPresent(true);
private @Nullable Slide getContentResolverSlideInfo(
Uri uri, int width, int height, int chatId) {
if (finalSlide.hasAudio()) {
audioView.setPlaybackViewModel(playbackViewModel);
audioView.setAudio((AudioSlide) finalSlide);
removableMediaView.display(audioView, false);
removableMediaView.addRemoveClickListener(
v -> playbackViewModel.stop(audioView.getMsgId()));
result.set(true);
} else if (finalSlide.isVcard()) {
vcardView.setVcard(
glideRequests, (VcardSlide) finalSlide, DcHelper.getRpc(context));
removableMediaView.display(vcardView, false);
} else if (finalSlide.hasDocument()) {
if (finalSlide.isWebxdcDocument()) {
DcMsg instance =
msg != null
? msg
: DcHelper.getContext(context).getMsg(finalSlide.dcMsgId);
webxdcView.setWebxdc(instance, context.getString(R.string.webxdc_draft_hint));
webxdcView.setWebxdcClickListener(
(v, s) -> WebxdcActivity.openWebxdcActivity(context, instance));
removableMediaView.display(webxdcView, false);
} else {
documentView.setDocument((DocumentSlide) finalSlide);
removableMediaView.display(documentView, false);
}
result.set(true);
} else {
Attachment attachment = finalSlide.asAttachment();
result.deferTo(
thumbnail.setImageResource(
glideRequests,
finalSlide,
attachment.getWidth(),
attachment.getHeight()));
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
}
long start = System.currentTimeMillis();
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
attachmentListener.onAttachmentChanged();
}
});
});
if (cursor != null && cursor.moveToFirst()) {
String fileName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
String mimeType = context.getContentResolver().getType(uri);
return result;
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
width = dimens.first;
height = dimens.second;
}
Log.w(
TAG,
"remote slide with size "
+ fileSize
+ " took "
+ (System.currentTimeMillis() - start)
+ "ms");
return mediaType.createSlide(
context, uri, fileName, mimeType, fileSize, width, height, chatId);
}
}
return null;
}
private @NonNull Slide getManuallyCalculatedSlideInfo(
Uri uri, int width, int height, @Nullable DcMsg msg) throws IOException {
long start = System.currentTimeMillis();
Long mediaSize = null;
String fileName = null;
String mimeType = null;
if (msg != null) {
fileName = msg.getFilename();
mimeType = msg.getFilemime();
}
if (PartAuthority.isLocalUri(uri)) {
mediaSize = PartAuthority.getAttachmentSize(context, uri);
if (fileName == null) fileName = PartAuthority.getAttachmentFileName(context, uri);
if (mimeType == null) mimeType = PartAuthority.getAttachmentContentType(context, uri);
}
if (mediaSize == null) {
mediaSize = MediaUtil.getMediaSize(context, uri);
}
if (mimeType == null) {
mimeType = MediaUtil.getMimeType(context, uri);
}
private @Nullable Slide getContentResolverSlideInfo(
Uri uri, int width, int height, int chatId, MediaType mediaType) {
long start = System.currentTimeMillis();
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
String fileName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
long mediaSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
String mimeType = context.getContentResolver().getType(uri);
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
@@ -381,14 +329,6 @@ public class AttachmentManager {
height = dimens.second;
}
if (fileName == null) {
try {
fileName = new File(uri.getPath()).getName();
} catch (Exception e) {
Log.w(TAG, "Could not get file name from uri: " + e);
}
}
Log.w(
TAG,
"local slide with size "
@@ -399,9 +339,61 @@ public class AttachmentManager {
return mediaType.createSlide(
context, uri, fileName, mimeType, mediaSize, width, height, chatId);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
return result;
return null;
}
private @NonNull Slide getManuallyCalculatedSlideInfo(
Uri uri, int width, int height, @Nullable DcMsg msg, MediaType mediaType, int chatId)
throws IOException {
long start = System.currentTimeMillis();
Long mediaSize = null;
String fileName = null;
String mimeType = null;
if (msg != null) {
fileName = msg.getFilename();
mimeType = msg.getFilemime();
}
if (PartAuthority.isLocalUri(uri)) {
mediaSize = PartAuthority.getAttachmentSize(context, uri);
if (fileName == null) fileName = PartAuthority.getAttachmentFileName(context, uri);
if (mimeType == null) mimeType = PartAuthority.getAttachmentContentType(context, uri);
}
if (mediaSize == null) {
mediaSize = MediaUtil.getMediaSize(context, uri);
}
if (mimeType == null) {
mimeType = MediaUtil.getMimeType(context, uri);
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
width = dimens.first;
height = dimens.second;
}
if (fileName == null) {
try {
fileName = new File(uri.getPath()).getName();
} catch (Exception e) {
Log.w(TAG, "Could not get file name from uri: " + e);
}
}
Log.w(
TAG,
"local slide with size "
+ mediaSize
+ " took "
+ (System.currentTimeMillis() - start)
+ "ms");
return mediaType.createSlide(
context, uri, fileName, mimeType, mediaSize, width, height, chatId);
}
// should be called when the attachment manager comes into view again.
@@ -447,7 +439,7 @@ public class AttachmentManager {
}
public static void selectDocument(Activity activity, int requestCode) {
selectMediaType(activity, "*/*", null, requestCode);
selectMediaType(activity, "*/*", null, requestCode, null, true);
}
public static void selectWebxdc(Activity activity, int requestCode) {
@@ -485,22 +477,35 @@ public class AttachmentManager {
.ifNecessary()
.withPermanentDenialDialog(
activity.getString(R.string.perm_explain_access_to_storage_denied))
.onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode))
.onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode, null, false))
.execute();
}
public static void selectLocation(Activity activity, int chatId) {
Context appContext = activity.getApplicationContext();
Rpc rpc = DcHelper.getRpc(appContext);
int accountId = DcHelper.getContext(appContext).getAccountId();
if (DcHelper.getContext(appContext).isSendingLocationsToChat(chatId)) {
boolean currentlySharing;
try {
currentlySharing = rpc.isSendingLocationsToChat(accountId, chatId);
} catch (RpcException e) {
Log.e(TAG, "Failed to check location streaming state", e);
return;
}
if (currentlySharing) {
if (LocationStreamingService.isRunning()) {
LocationStreamingService.stopSharing(appContext, chatId);
LocationStreamingService.stopSharing(appContext, accountId, chatId);
return;
}
// Stale service is dead but chat layer still thinks it's sharing.
// Clean up this chat and fall through to the fresh start flow.
ActiveLocationChats.remove(appContext, chatId);
DcHelper.getContext(appContext).sendLocationsToChat(chatId, 0);
// Clean up and fall through to the fresh start flow.
try {
rpc.sendLocationsToChat(accountId, chatId, 0);
} catch (RpcException e) {
Log.e(TAG, "Failed to stop stale location streaming", e);
}
}
Permissions.with(activity)
@@ -514,7 +519,8 @@ public class AttachmentManager {
ShareLocationDialog.show(
activity,
durationInSeconds ->
LocationStreamingService.startSharing(appContext, chatId, durationInSeconds));
LocationStreamingService.startSharing(
appContext, accountId, chatId, durationInSeconds));
})
.request(
android.Manifest.permission.ACCESS_FINE_LOCATION,
@@ -596,20 +602,6 @@ public class AttachmentManager {
.execute();
}
public static void selectMediaType(
Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
selectMediaType(activity, type, extraMimeType, requestCode, null, false);
}
public static void selectMediaType(
Activity activity,
@NonNull String type,
@Nullable String[] extraMimeType,
int requestCode,
@Nullable Uri initialUri) {
selectMediaType(activity, type, extraMimeType, requestCode, initialUri, false);
}
public static void selectMediaType(
Activity activity,
@NonNull String type,
@@ -713,13 +705,31 @@ public class AttachmentManager {
mimeType = "application/octet-stream";
}
DcContext dcContext = DcHelper.getContext(context);
switch (this) {
case IMAGE:
return new ImageSlide(context, uri, fileName, dataSize, width, height);
case GIF:
return new GifSlide(context, uri, fileName, dataSize, width, height);
case AUDIO:
return new AudioSlide(context, uri, dataSize, false, fileName);
DcMsg audioMsg = new DcMsg(dcContext, DcMsg.DC_MSG_AUDIO);
Attachment audioAttachment =
new UriAttachment(
uri,
null,
mimeType,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
dataSize,
0,
0,
fileName,
null,
false);
String audioPath = audioAttachment.getRealPath(context);
audioMsg.setFileAndDeduplicate(audioPath, fileName, mimeType);
dcContext.setDraft(chatId, audioMsg);
return new AudioSlide(context, audioMsg);
case VIDEO:
return new VideoSlide(context, uri, fileName, dataSize);
case DOCUMENT:
@@ -727,7 +737,6 @@ public class AttachmentManager {
// draft
// is set. Therefore we need to create a DcMsg already now.
if (fileName != null && fileName.endsWith(".xdc")) {
DcContext dcContext = DcHelper.getContext(context);
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC);
Attachment attachment =
new UriAttachment(
@@ -772,6 +781,10 @@ public class AttachmentManager {
updateVisibility();
}
public boolean isHidden() {
return hidden;
}
private void setAttachmentPresent(boolean isPresent) {
this.attachmentPresent = isPresent;
updateVisibility();
@@ -784,9 +797,9 @@ public class AttachmentManager {
} else {
vis = View.GONE;
}
if (vis == View.GONE && !attachmentViewStub.resolved()) {
if (attachmentView == null) {
return;
}
attachmentViewStub.get().setVisibility(vis);
attachmentView.setVisibility(vis);
}
}
@@ -78,4 +78,13 @@ public class SlideDeck {
}
return 0;
}
public int getAudioDraftId() {
for (Slide slide : slides) {
if (slide.hasAudio() && slide.dcMsgId != 0) {
return slide.dcMsgId;
}
}
return 0;
}
}
@@ -11,6 +11,7 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioAttributes;
import android.media.RingtoneManager;
@@ -23,13 +24,18 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.HashMap;
@@ -42,6 +48,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
@@ -61,8 +68,18 @@ public class NotificationCenter {
private volatile long lastAudibleNotification = 0;
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
// Map<accountId, Map<chatId, lines>, contains the last lines of each chat for each account
private final HashMap<Integer, HashMap<Integer, LinkedHashMap<Integer, String>>> inboxes =
private static class NotifData {
final Person sender;
final String text;
NotifData(Person sender, String text) {
this.sender = sender;
this.text = text;
}
}
// notification history of each chat for each account
private final HashMap<Integer, HashMap<Integer, LinkedHashMap<Integer, NotifData>>> inboxes =
new HashMap<>();
public NotificationCenter(Context context) {
@@ -410,31 +427,42 @@ public class NotificationCenter {
DcMsg dcMsg = dcContext.getMsg(msgId);
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
String shortLine =
DcContact sender = dcContext.getContact(dcMsg.getFromId());
String senderName = dcMsg.getSenderName(sender);
String personId = accountId + "-" + dcMsg.getFromId();
if (dcMsg.getOverrideSenderName() != null) {
// we need to treat the contact as a separate Person with different ID
// otherwise the name will be overwritten by future notifications
personId += "-" + senderName;
}
String text =
privacy.isDisplayMessage()
? dcMsg.getSummarytext(2000)
: context.getString(R.string.notify_new_message);
String shortLine = text;
if (dcChat.isMultiUser() && privacy.isDisplayContact()) {
shortLine =
dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + shortLine;
shortLine = senderName + ": " + text;
}
String tickerLine = shortLine;
if (!dcChat.isMultiUser() && privacy.isDisplayContact()) {
tickerLine =
dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + tickerLine;
if (dcMsg.getOverrideSenderName() != null) {
// There is an "overridden" display name on the message, so, we need to prepend the
// display name to the message,
// i.e. set the shortLine to be the same as the tickerLine.
shortLine = tickerLine;
}
NotifData notifData =
new NotifData(
new Person.Builder()
.setName(senderName)
.setIcon(getAvatarIcon(sender))
.setBot(sender.isBot())
.setKey(personId)
.build(),
text);
String tickerLine = text;
if (privacy.isDisplayContact()) {
tickerLine = senderName + ": " + text;
}
DcMsg quotedMsg = dcMsg.getQuotedMsg();
boolean isMention = dcChat.isMultiUser() && quotedMsg != null && quotedMsg.isOutgoing();
maybeAddNotification(accountId, dcChat, msgId, shortLine, tickerLine, true, isMention);
maybeAddNotification(accountId, dcChat, msgId, notifData, tickerLine, true, isMention);
});
}
@@ -450,16 +478,27 @@ public class NotificationCenter {
// just do nothing.
}
DcContact sender = dcContext.getContact(contactId);
String shortLine =
DcContact contact = dcContext.getContact(contactId);
String text =
context.getString(
R.string.reaction_by_other,
sender.getDisplayName(),
contact.getDisplayName(),
reaction,
dcMsg.getSummarytext(2000));
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
NotifData notifData =
new NotifData(
new Person.Builder()
.setName(contact.getDisplayName())
.setIcon(getAvatarIcon(contact))
.setBot(contact.isBot())
.setKey(accountId + "-" + contactId)
.build(),
text);
maybeAddNotification(
accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser());
accountId, dcChat, msgId, notifData, text, false, dcChat.isMultiUser());
});
}
@@ -473,6 +512,7 @@ public class NotificationCenter {
DcContext dcContext = context.getDcAccounts().getAccount(accountId);
DcMsg dcMsg = dcContext.getMsg(msgId);
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
DcMsg parentMsg;
if (dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) {
parentMsg = dcMsg;
@@ -486,10 +526,34 @@ public class NotificationCenter {
JSONObject info = parentMsg.getWebxdcInfo();
final String name = JsonUtils.optString(info, "name");
String shortLine = name.isEmpty() ? text : (name + ": " + text);
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
String tickerLine = name.isEmpty() ? text : (name + ": " + text);
NotifData notifData;
if (dcChat.isMultiUser()) {
byte[] blob = parentMsg.getWebxdcBlob(JsonUtils.optString(info, "icon"));
notifData =
new NotifData(
new Person.Builder()
.setName(name)
.setIcon(getAvatarIcon(blob))
.setKey(accountId + "-webxdc-" + msgId)
.build(),
text);
} else {
DcContact sender = dcContext.getContact(contactId);
notifData =
new NotifData(
new Person.Builder()
.setName(sender.getDisplayName())
.setIcon(getAvatarIcon(sender))
.setBot(sender.isBot())
.setKey(accountId + "-" + contactId)
.build(),
tickerLine);
}
maybeAddNotification(
accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser());
accountId, dcChat, msgId, notifData, tickerLine, false, dcChat.isMultiUser());
});
}
@@ -498,7 +562,7 @@ public class NotificationCenter {
int accountId,
DcChat dcChat,
int msgId,
String shortLine,
NotifData notifData,
String tickerLine,
boolean playInChatSound,
boolean isMention) {
@@ -538,20 +602,23 @@ public class NotificationCenter {
// the user may eg. have chosen a different sound
String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat);
LinkedHashMap<Integer, String> messagesForInbox = null;
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
LinkedHashMap<Integer, NotifData> messagesForInbox = null;
if (privacy.isDisplayContact()) {
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
if (accountInbox == null) {
accountInbox = new HashMap<>();
inboxes.put(accountId, accountInbox);
}
LinkedHashMap<Integer, String> messages = accountInbox.get(chatId);
LinkedHashMap<Integer, NotifData> messages = accountInbox.get(chatId);
if (messages == null) {
messages = new LinkedHashMap<>();
accountInbox.put(chatId, messages);
}
messages.put(msgId, shortLine);
if (!privacy.isDisplayMessage()) {
messages.clear();
}
messages.put(msgId, notifData);
messagesForInbox = new LinkedHashMap<>(messages);
}
}
@@ -564,7 +631,7 @@ public class NotificationCenter {
dcContext,
dcChat,
notificationChannel,
shortLine,
notifData.text,
tickerLine,
signal,
messagesForInbox,
@@ -583,7 +650,7 @@ public class NotificationCenter {
String contentText,
String ticker,
boolean signal,
LinkedHashMap<Integer, String> messagesForInbox,
LinkedHashMap<Integer, NotifData> messagesForInbox,
int messageCount,
boolean includeSummary) {
try {
@@ -658,7 +725,7 @@ public class NotificationCenter {
NotificationCompat.Action markAsReadAction =
new NotificationCompat.Action(
R.drawable.check, context.getString(R.string.mark_as_read_short), markReadIntent);
R.drawable.check, context.getString(R.string.mark_as_read), markReadIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NotificationCompat.Action replyAction =
@@ -694,14 +761,42 @@ public class NotificationCenter {
}
}
// Create inbox style (gets visible if the notification is expanded)
if (privacy.isDisplayContact() && privacy.isDisplayMessage() && messagesForInbox != null) {
// Create messaging style
if (privacy.isDisplayContact() && messagesForInbox != null) {
try {
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
for (String line : messagesForInbox.values()) {
inboxStyle.addLine(line);
Intent viewChatIntent = new Intent(context, ShareActivity.class);
viewChatIntent.setAction(Intent.ACTION_SEND);
viewChatIntent.putExtra(ShareActivity.EXTRA_ACC_ID, dcContext.getAccountId());
viewChatIntent.putExtra(ShareActivity.EXTRA_CHAT_ID, dcChat.getId());
ShortcutInfoCompat shortcut =
new ShortcutInfoCompat.Builder(
context, "chat-" + dcContext.getAccountId() + "-" + dcChat.getId())
.setShortLabel(dcChat.getName())
.setLongLived(true)
.setIntent(viewChatIntent)
.build();
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut);
builder.setShortcutInfo(shortcut);
DcContact selfContact = dcContext.getContact(DcContact.DC_CONTACT_ID_SELF);
Person self =
new Person.Builder()
.setName(selfContact.getDisplayName())
.setIcon(getAvatarIcon(selfContact))
.setKey(accountId + "-" + selfContact.getId())
.build();
NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle(self);
if (dcChat.isMultiUser()) {
style.setGroupConversation(true);
style.setConversationTitle(dcChat.getName());
}
builder.setStyle(inboxStyle);
for (Map.Entry<Integer, NotifData> msgEntry : messagesForInbox.entrySet()) {
long timestamp_ms = dcContext.getMsg(msgEntry.getKey()).getSortTimestamp() * 1000;
NotifData notifData = msgEntry.getValue();
style.addMessage(notifData.text, timestamp_ms, notifData.sender);
}
builder.setStyle(style);
} catch (Exception e) {
Log.w(TAG, e);
}
@@ -748,7 +843,7 @@ public class NotificationCenter {
@WorkerThread
private void rebuildNotification(
int accountId, int chatId, LinkedHashMap<Integer, String> messages) {
int accountId, int chatId, LinkedHashMap<Integer, NotifData> messages) {
try {
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accountId);
DcChat dcChat = dcContext.getChat(chatId);
@@ -766,9 +861,9 @@ public class NotificationCenter {
// Get the latest message ID (last entry in LinkedHashMap)
Integer latestMsgId = null;
String lastLine = null;
for (Map.Entry<Integer, String> entry : messages.entrySet()) {
for (Map.Entry<Integer, NotifData> entry : messages.entrySet()) {
latestMsgId = entry.getKey();
lastLine = entry.getValue();
lastLine = entry.getValue().text;
}
if (latestMsgId == null || lastLine == null) {
return;
@@ -797,8 +892,35 @@ public class NotificationCenter {
}
}
public Bitmap getAvatar(DcChat dcChat) {
Recipient recipient = new Recipient(context, dcChat);
private static @Nullable IconCompat getAvatarIcon(byte[] blob) {
if (blob == null) {
return null;
}
ByteArrayInputStream is = new ByteArrayInputStream(blob);
BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon");
Bitmap bitmap = drawable.getBitmap();
return IconCompat.createWithBitmap(bitmap);
}
private @Nullable IconCompat getAvatarIcon(DcChat dcChat) {
Bitmap avatar = getAvatar(dcChat);
return avatar != null ? IconCompat.createWithBitmap(avatar) : null;
}
private @Nullable IconCompat getAvatarIcon(DcContact dcContact) {
Bitmap avatar = getAvatar(dcContact);
return avatar != null ? IconCompat.createWithBitmap(avatar) : null;
}
private @Nullable Bitmap getAvatar(DcChat dcChat) {
return getAvatar(new Recipient(context, dcChat));
}
private @Nullable Bitmap getAvatar(DcContact dcContact) {
return getAvatar(new Recipient(context, dcContact));
}
private @Nullable Bitmap getAvatar(Recipient recipient) {
try {
Drawable drawable;
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
@@ -837,12 +959,12 @@ public class NotificationCenter {
public void removeNotification(int accountId, int chatId, int msgId) {
boolean shouldCancelNotification = false;
boolean removeSummary = false;
LinkedHashMap<Integer, String> remainingMessages = null;
LinkedHashMap<Integer, NotifData> remainingMessages = null;
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
if (accountInbox != null) {
LinkedHashMap<Integer, String> messages = accountInbox.get(chatId);
LinkedHashMap<Integer, NotifData> messages = accountInbox.get(chatId);
if (messages != null) {
messages.remove(msgId);
@@ -875,7 +997,7 @@ public class NotificationCenter {
public void removeNotifications(int accountId, int chatId) {
boolean removeSummary;
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
if (accountInbox == null) {
accountInbox = new HashMap<>();
}
@@ -901,7 +1023,7 @@ public class NotificationCenter {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
String tag = String.valueOf(accountId);
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
notificationManager.cancel(tag, ID_MSG_SUMMARY);
if (accountInbox != null) {
for (Integer chatId : accountInbox.keySet()) {
@@ -167,7 +167,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
notificationsEnabled.setChecked(!dcContext.isMuted());
notificationsEnabled.setSummary(getSummary(getContext(), false));
mentionNotifEnabled.setChecked(dcContext.isMentionsEnabled());
notifyCalls.setChecked(dcContext.getConfig("who_can_call_me") != "2");
notifyCalls.setChecked(!"2".equals(dcContext.getConfig("who_can_call_me")));
// set without altering "unset" state of the preference
reliableService.setOnPreferenceChangeListener(null);
@@ -18,6 +18,7 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.b44t.messenger.DcContext;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.zxing.BinaryBitmap;
@@ -120,10 +121,12 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
DcContext dcContext = DcHelper.getContext(this);
menu.clear();
getMenuInflater().inflate(R.menu.qr_show, menu);
menu.findItem(R.id.new_classic_contact)
.setVisible(!scanRelay && !DcHelper.getContext(this).isChatmail());
.setVisible(!scanRelay && dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0);
Util.redMenuItem(menu, R.id.withdraw);
if (tabLayout.getSelectedTabPosition() == TAB_SCAN) {
@@ -314,6 +314,12 @@ public class QrCodeHandler {
private void showFingerprintOrQrSuccess(
AlertDialog.Builder builder, DcLot qrParsed, String name) {
if (qrParsed.getState() == DcContext.DC_QR_ADDR
&& dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 1) {
DcHelper.prepareInvalidUnencryptedDialog(activity, builder);
return;
}
@StringRes
int resId =
qrParsed.getState() == DcContext.DC_QR_ADDR
@@ -70,11 +70,12 @@ public class EditRelayActivity extends BaseActionBarActivity
private Group advancedGroup;
private ImageView advancedIcon;
private View imapFolderLayout;
private boolean imapFolderHasValue = false;
private ProgressDialog progressDialog;
private boolean cancelled = false;
private View imapFolderLayout;
private boolean showImapFolder = false;
Spinner imapSecurity;
Spinner smtpSecurity;
Spinner certCheck;
@@ -201,8 +202,8 @@ public class EditRelayActivity extends BaseActionBarActivity
if (config.imapPort != null) imapPortInput.setText(config.imapPort.toString());
expandAdvanced = expandAdvanced || config.imapPort != null;
imapFolderHasValue = !TextUtils.isEmpty(config.imapFolder);
if (imapFolderHasValue) {
showImapFolder = !TextUtils.isEmpty(config.imapFolder);
if (showImapFolder) {
imapFolderInput.setText(config.imapFolder);
expandAdvanced = true;
}
@@ -407,7 +408,7 @@ public class EditRelayActivity extends BaseActionBarActivity
advancedIcon.setRotation(45);
} else {
advancedGroup.setVisibility(View.VISIBLE);
imapFolderLayout.setVisibility(imapFolderHasValue ? View.VISIBLE : View.GONE);
imapFolderLayout.setVisibility(showImapFolder ? View.VISIBLE : View.GONE);
advancedIcon.setRotation(0);
}
}
@@ -510,7 +511,7 @@ public class EditRelayActivity extends BaseActionBarActivity
param.password = getParam(R.id.password_text, false);
param.imapServer = getParam(R.id.imap_server_text, true);
param.imapPort = Util.objectToInt(getParam(R.id.imap_port_text, true));
param.imapFolder = getParam(R.id.imap_folder_text, false);
param.imapFolder = getParam(R.id.imap_folder_text, true);
param.imapSecurity = socketSecurityFromInt(imapSecurity.getSelectedItemPosition());
param.imapUser = getParam(R.id.imap_login_text, false);
param.smtpServer = getParam(R.id.smtp_server_text, true);
@@ -22,6 +22,7 @@ class SearchViewModel extends ViewModel {
private final Context appContext;
private final DcContext dcContext;
private boolean forwarding = false;
private final Object bgSearchLock = new Object();
private boolean inBgSearch;
private boolean needsAnotherBgSearch;
@@ -45,27 +46,29 @@ class SearchViewModel extends ViewModel {
}
public void updateQuery() {
if (inBgSearch) {
needsAnotherBgSearch = true;
Log.i(TAG, "... search call debounced");
} else {
synchronized (bgSearchLock) {
if (inBgSearch) {
Log.i(TAG, "... search call debounced");
needsAnotherBgSearch = true;
return;
}
inBgSearch = true;
Util.runOnBackground(
() -> {
Util.sleep(100);
needsAnotherBgSearch = false;
queryAndCallback(lastQuery, searchResult::postValue);
while (needsAnotherBgSearch) {
Util.sleep(100);
needsAnotherBgSearch = false;
Log.i(TAG, "... executing debounced search call");
queryAndCallback(lastQuery, searchResult::postValue);
}
inBgSearch = false;
});
}
Util.runOnBackground(
() -> {
while (true) {
Log.i(TAG, "... executing debounced search call");
queryAndCallback(lastQuery, searchResult::postValue);
synchronized (bgSearchLock) {
if (!needsAnotherBgSearch) {
inBgSearch = false;
return;
}
needsAnotherBgSearch = false;
}
Util.sleep(100);
}
});
}
private void queryAndCallback(@NonNull String query, @NonNull SearchViewModel.Callback callback) {
@@ -57,22 +57,26 @@ public class LongClickCopySpan extends ClickableSpan {
DcContext dcContext = DcHelper.getContext(activity);
int contactId = dcContext.lookupContactIdByAddr(addr);
if (contactId == 0 && dcContext.mayBeValidAddr(addr)) {
contactId = dcContext.createContact(null, addr);
}
DcContact contact = dcContext.getContact(contactId);
if (contact.getId() != 0
DcContact contact = (contactId != 0) ? dcContext.getContact(contactId) : null;
if (contact != null
&& !contact.isBlocked()
&& dcContext.getChatIdByContactId(contact.getId()) != 0) {
openChat(activity, contact);
} else if (contact == null
&& dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 1) {
DcHelper.showInvalidUnencryptedDialog(activity);
} else {
String name = contact != null ? contact.getDisplayName() : addr;
new AlertDialog.Builder(activity)
.setMessage(
activity.getString(R.string.ask_start_chat_with, contact.getDisplayName()))
.setMessage(activity.getString(R.string.ask_start_chat_with, name))
.setPositiveButton(
android.R.string.ok,
(dialog, which) -> {
openChat(activity, contact);
openChat(
activity,
contact == null
? dcContext.getContact(dcContext.createContact(null, addr))
: contact);
})
.setNegativeButton(R.string.cancel, null)
.show();
@@ -15,7 +15,6 @@ public class ShareUtil {
private static final String FORWARDED_MESSAGE_ACCID = "forwarded_message_accid";
private static final String FORWARDED_MESSAGE_IDS = "forwarded_message_ids";
private static final String SHARED_URIS = "shared_uris";
private static final String SHARED_CONTACT_ID = "shared_contact_id";
private static final String IS_SHARING = "is_sharing";
private static final String IS_FROM_WEBXDC = "is_from_webxdc";
private static final String SHARED_TITLE = "shared_title";
@@ -112,15 +111,6 @@ public class ShareUtil {
}
}
public static int getSharedContactId(Activity activity) {
try {
return activity.getIntent().getIntExtra(SHARED_CONTACT_ID, 0);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
public static String getSharedText(Activity activity) {
try {
return activity.getIntent().getStringExtra(TEXT_EXTRA);
@@ -142,7 +132,6 @@ public class ShareUtil {
activity.getIntent().removeExtra(FORWARDED_MESSAGE_ACCID);
activity.getIntent().removeExtra(FORWARDED_MESSAGE_IDS);
activity.getIntent().removeExtra(SHARED_URIS);
activity.getIntent().removeExtra(SHARED_CONTACT_ID);
activity.getIntent().removeExtra(IS_SHARING);
activity.getIntent().removeExtra(DIRECT_SHARING_CHAT_ID);
activity.getIntent().removeExtra(TEXT_EXTRA);
@@ -167,9 +156,6 @@ public class ShareUtil {
if (!getSharedUris(currentActivity).isEmpty()) {
newActivityIntent.putParcelableArrayListExtra(SHARED_URIS, getSharedUris(currentActivity));
}
if (getSharedContactId(currentActivity) != 0) {
newActivityIntent.putExtra(SHARED_CONTACT_ID, getSharedContactId(currentActivity));
}
if (getSharedText(currentActivity) != null) {
newActivityIntent.putExtra(TEXT_EXTRA, getSharedText(currentActivity));
}
@@ -220,11 +206,6 @@ public class ShareUtil {
composeIntent.putExtra(IS_SHARING, true);
}
public static void setSharedContactId(Intent composeIntent, int contactId) {
composeIntent.putExtra(SHARED_CONTACT_ID, contactId);
composeIntent.putExtra(IS_SHARING, true);
}
public static void setSharedTitle(Intent composeIntent, String text) {
composeIntent.putExtra(SHARED_TITLE, text);
}
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="translationY"
android:valueType="floatType"
android:valueFrom="0dp"
android:valueTo="-8dp"
android:duration="1500"
android:repeatMode="reverse"
android:repeatCount="infinite"
android:interpolator="@android:interpolator/accelerate_decelerate" />
</set>
+13
View File
@@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="228dp"
android:height="280dp"
android:viewportWidth="228"
android:viewportHeight="280">
<path
android:pathData="m10.03,234.14c0.3,-0.01 0.6,-0.02 0.9,-0.04 -0.07,-0.49 -0.14,-0.97 -0.2,-1.46 -0.15,-1.34 -0.26,-2.69 -0.25,-4.04 -0.02,-0.86 -0.05,-1.71 -0.07,-2.57 -0.09,-0.06 -0.18,-0.13 -0.27,-0.19 -0.02,-0.02 -0.04,-0.03 -0.07,-0.05zM44.87,232.95c11.71,-8.35 26.86,-14.79 46.21,-15.9 0,0 39.93,-0.27 47.91,-3.53 7.98,-3.26 68.68,-14.69 82.94,-98.43 14.26,-83.74 -1.06,-115.09 -1.06,-115.09 0,0 -21.14,55.68 -81.02,59.81 0,0 -14.5,1.03 -38.82,1.42 -24.32,0.39 -75.77,20.65 -90.55,85.62l-0.22,43.44c2.5,4.22 5.49,8.12 8.91,11.66 3.99,4.11 8.11,8.12 12.79,11.45 2.26,1.65 4.65,3.2 6.51,5.33 1.94,2.34 3.33,5 4.2,7.93 0.71,2.1 1.45,4.2 2.2,6.28z"
android:fillColor="#ffffff" />
<path
android:pathData="m217.97,45.86c-0.3,0.01 -0.6,0.02 -0.9,0.04 0.07,0.49 0.14,0.97 0.2,1.46 0.15,1.34 0.26,2.69 0.25,4.04 0.02,0.86 0.05,1.71 0.07,2.57 0.09,0.06 0.18,0.13 0.27,0.19 0.02,0.02 0.04,0.03 0.07,0.05zM183.13,47.05c-11.71,8.35 -26.86,14.79 -46.21,15.9 0,0 -39.93,0.27 -47.91,3.53 -7.98,3.26 -68.68,14.69 -82.94,98.43 -14.26,83.74 1.06,115.09 1.06,115.09 0,0 21.14,-55.68 81.02,-59.81 0,0 14.5,-1.03 38.82,-1.42 24.32,-0.39 75.77,-20.65 90.55,-85.62l0.22,-43.44c-2.5,-4.22 -5.49,-8.12 -8.91,-11.66 -3.99,-4.11 -8.11,-8.12 -12.79,-11.45 -2.26,-1.65 -4.65,-3.2 -6.51,-5.33 -1.94,-2.34 -3.33,-5 -4.2,-7.93 -0.71,-2.1 -1.45,-4.2 -2.2,-6.28z"
android:fillColor="#ffffff" />
</vector>
+12
View File
@@ -0,0 +1,12 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="490.3dp"
android:height="72.64dp"
android:viewportWidth="490.3"
android:viewportHeight="72.64">
<path
android:pathData="M17.45,58.55L50.75,58.55L49.76,46.94L18.53,46.94ZM33.92,30.83 L42.83,51.26 42.29,55.13 49.85,70.7L67.13,70.7L33.92,3.74 0.81,70.7L17.99,70.7L25.82,54.5 25.1,51.17ZM85.75,29.3L72.35,29.3v41.4h13.41zM97.72,43.34 L103.57,31.73q-1.08,-1.62 -3.15,-2.52 -2.07,-0.9 -4.32,-0.9 -3.42,0 -6.57,2.25 -3.15,2.25 -5.13,6.12 -1.89,3.87 -1.89,8.82l3.24,4.77q0,-2.97 0.81,-5.04 0.81,-2.07 2.34,-3.15 1.53,-1.08 3.51,-1.08 1.8,0 2.97,0.63 1.26,0.63 2.34,1.71zM119.41,50q0,-3.06 1.44,-5.49 1.44,-2.43 3.96,-3.87 2.61,-1.44 5.58,-1.44 2.25,0 4.59,0.72 2.34,0.72 4.32,2.07 2.07,1.35 3.33,3.33L142.63,32q-2.16,-1.53 -5.4,-2.52 -3.24,-1.08 -7.92,-1.08 -6.84,0 -12.33,2.7 -5.4,2.7 -8.55,7.56 -3.06,4.77 -3.06,11.34 0,6.48 3.06,11.34 3.15,4.86 8.55,7.56 5.49,2.7 12.33,2.7 4.68,0 7.92,-0.99 3.24,-1.08 5.4,-2.79L142.63,54.59q-1.26,1.98 -3.24,3.33 -1.89,1.35 -4.23,2.16 -2.25,0.72 -4.77,0.72 -2.97,0 -5.58,-1.35 -2.52,-1.35 -3.96,-3.69 -1.44,-2.43 -1.44,-5.76zM162.97,57.74q0,-1.53 0.72,-2.52 0.72,-1.08 2.16,-1.62 1.44,-0.54 3.87,-0.54 3.15,0 5.94,0.9 2.88,0.81 5.04,2.43v-6.03q-1.08,-1.17 -3.24,-2.25 -2.16,-1.08 -5.22,-1.8 -2.97,-0.72 -6.75,-0.72 -7.74,0 -11.97,3.51 -4.23,3.42 -4.23,9.36 0,4.23 1.98,7.2 2.07,2.97 5.4,4.5 3.42,1.44 7.38,1.44 3.96,0 7.38,-1.35 3.42,-1.44 5.58,-4.14 2.16,-2.79 2.16,-6.75l-1.44,-5.4q0,2.88 -1.26,4.86 -1.26,1.98 -3.24,2.97 -1.98,0.99 -4.32,0.99 -1.62,0 -2.97,-0.54 -1.35,-0.63 -2.16,-1.71 -0.81,-1.17 -0.81,-2.79zM156.94,41.99q0.9,-0.54 2.88,-1.35 1.98,-0.9 4.59,-1.53 2.7,-0.63 5.49,-0.63 1.89,0 3.33,0.36 1.53,0.36 2.52,1.17 0.99,0.81 1.44,1.98 0.54,1.08 0.54,2.61v26.1h13.05L190.77,41.54q0,-4.32 -2.52,-7.29 -2.52,-2.97 -6.84,-4.5 -4.32,-1.62 -9.81,-1.62 -5.85,0 -10.8,1.53 -4.95,1.53 -8.46,3.24zM227.85,45.5v25.2h14.13L241.98,44.06q0,-7.47 -3.51,-11.61 -3.51,-4.14 -11.25,-4.14 -4.59,0 -7.74,1.89 -3.06,1.89 -4.86,5.31L214.62,29.3L200.94,29.3v41.4h13.68L214.62,45.5q0,-2.43 0.9,-4.14 0.9,-1.71 2.52,-2.61 1.62,-0.9 3.78,-0.9 3.24,0 4.59,1.98 1.44,1.98 1.44,5.67zM273.74,71.6q7.92,0 13.41,-3.06 5.58,-3.06 8.82,-9.18l-12.15,-2.97q-1.53,2.7 -4.14,4.05 -2.52,1.35 -6.12,1.35 -3.15,0 -5.31,-1.35 -2.16,-1.44 -3.24,-4.14 -1.08,-2.7 -1.08,-6.48 0.09,-4.14 1.08,-6.84 1.08,-2.79 3.15,-4.14 2.07,-1.35 5.13,-1.35 2.43,0 4.23,1.08 1.8,1.08 2.79,3.06 0.99,1.98 0.99,4.68 0,0.63 -0.36,1.62 -0.27,0.9 -0.72,1.53l3.24,-4.14h-25.56v7.56h37.8q0.18,-0.63 0.18,-1.53 0,-0.99 0,-1.98 0,-6.66 -2.61,-11.34 -2.61,-4.68 -7.56,-7.11 -4.95,-2.52 -12.15,-2.52 -7.2,0 -12.51,2.61 -5.22,2.61 -8.01,7.47 -2.79,4.86 -2.79,11.52 0,6.57 2.88,11.43 2.88,4.86 8.1,7.56 5.31,2.61 12.51,2.61zM318.56,39.2q0,-5.85 2.61,-9.99 2.61,-4.14 6.84,-6.3 4.32,-2.16 9.45,-2.16 4.5,0 7.92,1.17 3.42,1.08 6.12,3.06 2.7,1.89 4.77,4.05L356.26,12.65q-3.78,-2.97 -8.28,-4.68 -4.5,-1.71 -11.43,-1.71 -7.56,0 -13.95,2.34 -6.39,2.34 -10.98,6.75 -4.59,4.41 -7.11,10.44 -2.52,6.03 -2.52,13.41 0,7.38 2.52,13.41 2.52,6.03 7.11,10.44 4.59,4.41 10.98,6.75 6.39,2.34 13.95,2.34 6.93,0 11.43,-1.71 4.5,-1.71 8.28,-4.68L356.26,49.37q-2.07,2.16 -4.77,4.05 -2.7,1.89 -6.12,3.06 -3.42,1.17 -7.92,1.17 -5.13,0 -9.45,-2.16 -4.23,-2.16 -6.84,-6.3 -2.61,-4.23 -2.61,-9.99zM381.01,0.5L367.33,0.5L367.33,70.7h13.68zM394.24,45.5v25.2h13.59L407.83,43.52q0,-5.13 -1.44,-8.64 -1.44,-3.51 -4.59,-5.31 -3.06,-1.8 -8.19,-1.8 -5.13,0 -8.46,2.43 -3.24,2.34 -4.86,6.39 -1.62,3.96 -1.62,8.91h2.34q0,-2.43 0.9,-4.05 0.9,-1.71 2.52,-2.61 1.62,-0.99 3.78,-0.99 3.33,0 4.68,1.98 1.35,1.98 1.35,5.67zM428.97,57.74q0,-1.53 0.72,-2.52 0.72,-1.08 2.16,-1.62 1.44,-0.54 3.87,-0.54 3.15,0 5.94,0.9 2.88,0.81 5.04,2.43v-6.03q-1.08,-1.17 -3.24,-2.25 -2.16,-1.08 -5.22,-1.8 -2.97,-0.72 -6.75,-0.72 -7.74,0 -11.97,3.51 -4.23,3.42 -4.23,9.36 0,4.23 1.98,7.2 2.07,2.97 5.4,4.5 3.42,1.44 7.38,1.44 3.96,0 7.38,-1.35 3.42,-1.44 5.58,-4.14 2.16,-2.79 2.16,-6.75l-1.44,-5.4q0,2.88 -1.26,4.86 -1.26,1.98 -3.24,2.97 -1.98,0.99 -4.32,0.99 -1.62,0 -2.97,-0.54 -1.35,-0.63 -2.16,-1.71 -0.81,-1.17 -0.81,-2.79zM422.94,41.99q0.9,-0.54 2.88,-1.35 1.98,-0.9 4.59,-1.53 2.7,-0.63 5.49,-0.63 1.89,0 3.33,0.36 1.53,0.36 2.52,1.17 0.99,0.81 1.44,1.98 0.54,1.08 0.54,2.61v26.1h13.05L456.78,41.54q0,-4.32 -2.52,-7.29 -2.52,-2.97 -6.84,-4.5 -4.32,-1.62 -9.81,-1.62 -5.85,0 -10.8,1.53 -4.95,1.53 -8.46,3.24zM462.09,29.3L462.09,40.55L489.8,40.55L489.8,29.3ZM469.38,14.9L469.38,70.7L482.43,70.7L482.43,14.9Z"
android:strokeWidth="0.999939"
android:fillColor="#ffffff"
android:strokeColor="#ffffff" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#7b00c8"
android:endColor="#5564da"
android:angle="315" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/input_panel_bg_color" />
<corners android:radius="25dp" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="8dp" />
</shape>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2px"
android:color="@color/white" />
<corners android:radius="8dp" />
</shape>
@@ -224,7 +224,6 @@
android:id="@+id/imap_folder"
android:layout_width="0dp"
android:layout_height="58dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
app:layout_constraintTop_toBottomOf="@id/imap_port">
@@ -234,7 +233,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:hint="@string/login_imap_folder" />
android:hint="IMAP folder" />
</com.google.android.material.textfield.TextInputLayout>
@@ -36,20 +36,19 @@
android:layout_height="0dp"
android:layout_weight="1" />
<ViewStub
android:id="@+id/attachment_editor_stub"
android:inflatedId="@+id/attachment_editor"
android:layout="@layout/conversation_activity_attachment_editor_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView
android:id="@+id/conversation_activity_message_request_bottom_bar"
android:background="?android:attr/windowBackground"
android:background="@drawable/input_panel_rounded_bg"
android:elevation="1dp"
android:outlineProvider="background"
android:visibility="gone"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:paddingLeft="16dp"
android:paddingStart="16dp"
android:paddingRight="16dp"
@@ -5,7 +5,6 @@
android:id="@+id/attachment_editor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/input_panel_bg_color"
android:gravity="center_horizontal"
android:visibility="gone">
@@ -29,9 +28,9 @@
android:contentDescription="@string/menu_add_attachment"
app:thumbnail_radius="@dimen/message_corner_radius"
app:minWidth="100dp"
app:maxWidth="300dp"
app:maxWidth="200dp"
app:minHeight="100dp"
app:maxHeight="300dp" />
app:maxHeight="200dp" />
<org.thoughtcrime.securesms.components.audioplay.AudioView
android:id="@+id/attachment_audio"
@@ -29,9 +29,9 @@
<View
android:id="@+id/bottom_divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_height="0dp"
android:layout_gravity="bottom"
android:background="@drawable/compose_divider_background"
android:background="@android:color/transparent"
android:alpha="1" />
<ImageButton
@@ -8,11 +8,17 @@
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:clickable="true"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:clipChildren="false"
android:clipToPadding="false"
android:background="?attr/input_panel_bg_color">
android:background="@drawable/input_panel_rounded_bg"
android:elevation="1dp"
android:outlineProvider="background">
<org.thoughtcrime.securesms.components.QuoteView
android:id="@+id/quote_view"
@@ -25,6 +31,14 @@
app:message_type="preview"
tools:visibility="visible" />
<include
layout="@layout/conversation_activity_attachment_editor_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/quote_view" />
<EditText
android:id="@+id/subject_text"
style="@style/ComposeEditText"
@@ -32,6 +46,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:hint="@string/subject"
android:inputType="text"
@@ -39,7 +54,7 @@
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/quote_view"
app:layout_constraintTop_toBottomOf="@id/attachment_editor"
tools:visibility="visible" />
<FrameLayout
@@ -69,11 +84,11 @@
android:id="@+id/emoji_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_gravity="bottom"
android:minHeight="40dp"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:background="@drawable/touch_highlight_background"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:background="@drawable/circle_touch_highlight_background"
android:contentDescription="@string/menu_toggle_keyboard" />
<org.thoughtcrime.securesms.components.ComposeText
@@ -105,11 +120,11 @@
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:src="?quick_camera_icon"
android:paddingLeft="11dp"
android:paddingRight="11dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:background="@drawable/touch_highlight_background"
android:background="@drawable/circle_touch_highlight_background"
android:contentDescription="@string/camera" />
<org.thoughtcrime.securesms.components.MicrophoneRecorderView
@@ -136,15 +151,14 @@
<org.thoughtcrime.securesms.components.AnimatingToggle
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/input_field_frame_layout"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/button_toggle"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="6dp"
android:background="@drawable/send_button_bg"
android:layout_gravity="center_vertical">
android:layout_gravity="bottom">
<ImageButton
android:id="@+id/attach_button"
@@ -4,7 +4,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="8dp"
android:layout_margin="4dp"
android:src="@drawable/conversation_attachment_edit"
android:contentDescription="@string/global_menu_edit_desktop"
android:visibility="gone" />
+23 -22
View File
@@ -4,10 +4,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_welcome_gradient"
tools:context=".WelcomeActivity">
<include layout="@layout/status_bar_background" />
<LinearLayout
android:id="@+id/content_container"
android:layout_width="match_parent"
@@ -16,7 +15,7 @@
android:gravity="center_horizontal">
<androidx.legacy.widget.Space
android:layout_weight="3"
android:layout_weight="4"
android:layout_width="match_parent"
android:layout_height="0dp" />
@@ -24,23 +23,25 @@
android:id="@+id/welcome_icon"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_weight="10"
android:gravity="bottom"
android:src="@drawable/intro1"
android:paddingLeft="32dp"
android:paddingRight="32dp"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/welcome_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="4"
android:gravity="center"
android:padding="8dp"
android:src="@drawable/app_logo"
android:paddingLeft="35dp"
android:paddingRight="35dp"
android:scaleType="fitCenter"
android:importantForAccessibility="no" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:src="@drawable/app_name_logo"
android:paddingLeft="35dp"
android:paddingRight="35dp"
android:scaleType="fitCenter"
android:layout_marginBottom="20dp"
android:text="@string/welcome_chat_over_email"
android:textSize="22sp"
android:textStyle="bold" />
android:importantForAccessibility="no" />
<LinearLayout
android:orientation="vertical"
@@ -50,7 +51,7 @@
android:gravity="center_horizontal">
<Button
style="@style/ButtonPrimary"
style="@style/WhiteButtonPrimary"
android:id="@+id/signup_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -60,7 +61,7 @@
android:text="@string/onboarding_create_instant_account" />
<Button
style="@style/ButtonSecondary"
style="@style/WhiteButtonSecondary"
android:id="@+id/add_as_second_device_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -70,7 +71,7 @@
android:text="@string/multidevice_receiver_title" />
<Button
style="@style/ButtonSecondary"
style="@style/WhiteButtonSecondary"
android:id="@+id/backup_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -82,7 +83,7 @@
</LinearLayout>
<androidx.legacy.widget.Space
android:layout_weight="3"
android:layout_weight="4"
android:layout_width="match_parent"
android:layout_height="0dp" />
+4
View File
@@ -70,6 +70,10 @@ window.webxdc = (() => {
selfName: InternalJSApi.selfName(),
isAppSender: InternalJSApi.isAppSender(),
isBroadcast: InternalJSApi.isBroadcast(),
joinRealtimeChannel: () => {
realtimeChannel = createRealtimeChannel();
InternalJSApi.sendRealtimeAdvertisement();
+57 -2
View File
@@ -25,6 +25,8 @@
<string name="downloading">S\'està baixant...</string>
<string name="open_attachment">Obre l\'adjunt</string>
<string name="join">Uneix-te</string>
<string name="join_group">Unir-se al Grup</string>
<string name="join_channel">Unir-se al Canal</string>
<string name="rejoin">Torna a unir-te</string>
<string name="delete">Esborra</string>
<string name="delete_for_me">Esborra per a mi</string>
@@ -72,6 +74,8 @@
<string name="mark_as_read">Marca-ho com a llegit</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being read". In english, this could be "Read" (past tense of "to read"), in german, this could be "Gelesen". -->
<string name="mark_as_read_short">Llegit</string>
<!-- Used as an menu entry or button -->
<string name="mark_as_unread">Marcar com a no llegit</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being unread". In english, this could be "Unread" (past tense of "to unread"), in german, this could be "Ungelesen". -->
<string name="mark_as_unread_short">No llegit</string>
<!-- Placeholder text when something is loading -->
@@ -94,6 +98,7 @@
<string name="offline">Fora de línia</string>
<!-- For the next view or as "continue". Should be as short as possible. -->
<string name="next">Següent</string>
<string name="warning">Atenció</string>
<string name="error">Error</string>
<string name="error_x">Error: %1$s</string>
<string name="no_app_to_handle_data">No sé trobar cap aplicació per a gestionar aquest tipus de dades.</string>
@@ -203,6 +208,8 @@
<string name="capture">Captura</string>
<!-- Switch eg. from front to back camera -->
<string name="switch_camera">Canvia la càmera</string>
<!-- Toggle camera on/off eg. during an video call -->
<string name="toggle_camera">Commuta Càmera</string>
<string name="toggle_fullscreen">Alterna el mode de pantalla completa</string>
<string name="location">Ubicació</string>
<string name="locations">Ubicacions</string>
@@ -265,7 +272,7 @@
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channels">Canals</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="new_channel">Nou Canal</string>
<string name="new_channel">Canal nou</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel_name">Nom del Canal</string>
<!-- The number of times a message is shown in channels. Use what other messengers are using here, smth. as "N-times seen" or "1 Aufruf" (german) work as well, where "1 impression" or "1 hit" may sound too formal and may be more marketing slang. -->
@@ -386,6 +393,8 @@
<string name="share_location_for_one_hour">Durant 1 hora</string>
<string name="share_location_for_two_hours">Durant 2 hores</string>
<string name="share_location_for_six_hours">Durant 6 hores</string>
<string name="share_location_for_24_hours">Durant 24 hores</string>
<plurals name="ask_send_following_n_files_to">
<item quantity="one">Voleu enviar el següent fitxer a %s?</item>
<item quantity="other">Voleu enviar els següents %d fitxers a %s?</item>
@@ -402,6 +411,8 @@
<string name="answer_call">Respon</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Rebutja</string>
<!-- the action to end a call -->
<string name="end_call2">Finalitza trucada</string>
<!-- Used in call bubbles, summaries, notifications -->
<string name="audio_call">Trucada de veu</string>
<!-- Used in call bubbles, summaries, notifications -->
@@ -424,6 +435,15 @@
<string name="canceled_call">Trucada cancel·lada</string>
<string name="missed_call">Trucada perduda</string>
<string name="already_in_call">En una trucada</string>
<string name="call_answered_elsewhere">Trucada agafada en un altre dispositiu</string>
<string name="call_requires_camera_permission">Permís de Càmera és necessari per a video trucades</string>
<string name="call_requires_mic_permission">Permís de Micrófon necessari per a trucades.</string>
<string name="call_requires_connection">No es pot iniciar la trucada. Assegura\'t que el teu dispositiu té connexió a internet i torna-ho a intentar.</string>
<!-- Used e.g. in a notifications to describe an ongoing call. "Call" is a noun here, in the meaning of "This is the call with ..." where the placeholder is replaced by the name of the contact. -->
<string name="call_with">Truca amb %1$s</string>
<!-- Use e.g. in a notifications during ringing. Placeholder is relaced by the name of the contact being called. -->
<string name="calling_person">Trucant %1$s…</string>
<!-- get confirmations -->
<!-- confirmation for leaving groups or channels. If a subject is needed, "Are you sure you want to leave the chat?" would work as well -->
<string name="ask_leave_group">Segur que vols sortir?</string>
@@ -437,6 +457,11 @@
<item quantity="one">¿Esborra %d missatge de tots els teus dispositius?</item>
<item quantity="other">¿Esborra %d missatges de tots els teus dispositius?</item>
</plurals>
<!-- First placeholder gets replaced by the number of messages to forward. Second placeholder gets replaced by the name of the destination chat -->
<plurals name="ask_forward_messages">
<item quantity="one">Reenvia missatges a %2$s?</item>
<item quantity="other">Reenvia %1$d missatges a %2$s?</item>
</plurals>
<!-- deprecated, use ask_forward_messages -->
<string name="ask_forward">Reenvia missatges a %1$s?</string>
<string name="ask_forward_multiple">Voleu reenviar els missatges a %1$d xats?</string>
@@ -526,8 +551,11 @@
<!-- search -->
<string name="search">Busca</string>
<string name="search_in_chat">Busca al xat</string>
<!-- Placeholder will be replaced by the chat name -->
<string name="search_in">Busca a %1$s</string>
<string name="search_files">Busca els fitxers</string>
<string name="search_explain">Busca als xats, contactes i missatges</string>
<string name="search_result_for_x">Resultat per \"%1$s\"</string>
<string name="search_no_result_for_x">No s\'han trobat resultats per \"%s\"</string>
<!-- Adjective, as in "Show Unread Messages" -->
<string name="search_unread">Sense llegir</string>
@@ -540,6 +568,7 @@
<string name="group_create_button">Crea el grup</string>
<!-- deprecated, use please_enter_chat_name -->
<string name="group_please_enter_group_name">Si us plau, poseu nom al grup.</string>
<string name="please_enter_chat_name">Si us plau, introdueix un nom.</string>
<string name="group_add_members">Afegiu-hi membres</string>
<string name="group_self_not_in_group">Has de ser membre del grup per poder fer això.</string>
<string name="profile_encryption">Encriptació</string>
@@ -644,6 +673,7 @@
<string name="welcome_chat_over_email">Xat segur i descentralitzat</string>
<string name="scan_invitation_code">Escaneja el Codi d\'Invitació</string>
<string name="login_title">Identifiqueu-vos</string>
<string name="login_advanced_hint">Aquest inici de sessió és per usuaris avançats.\n\nNo utilitzis una adreça que ja estiguis utilitzant en una altra aplicació.</string>
<string name="login_inbox">Safata d\'entrada</string>
<string name="login_imap_login">Nom d\'identificació IMAP</string>
<string name="login_imap_server">Servidor IMAP</string>
@@ -678,6 +708,7 @@
<string name="used_for_sending">Utilitzat per enviar</string>
<string name="hide_from_contacts">Amagar als contactes</string>
<string name="hidden_from_contacts">Amagat als contactes</string>
<string name="enforce_e2ee">Força el xifrat per tots els Retransmissors</string>
<!-- Hint for the list of relays -->
<string name="transport_list_hint">Els missatges es reben en tots els retransmissors. ⚠️ Si canvies qualsevol cosa aquí, assegura\'t que tots els teus dispositius funcionin com a mínim amb la versió 2.47.0. Sinó, els dispositius més antics podrien no rebre els missatges.</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
@@ -777,6 +808,8 @@
<string name="pref_incognito_keyboard_explain">Sol·licita al teclat que deshabiliti l\'aprenentatge personalitzat</string>
<string name="pref_read_receipts">Confirmació de lectura</string>
<string name="pref_read_receipts_explain">Si desactiveu la confirmació de lectura dels vostres missatges, tampoc no rebreu la dels altres usuaris.</string>
<!-- Title above a list of contacts who read a message -->
<string name="read_by">Llegit per</string>
<string name="pref_server">Servidor</string>
<string name="pref_encryption">Xifratge</string>
<string name="pref_manage_keys">Gestió de les claus</string>
@@ -899,9 +932,12 @@
<string name="group_name_changed_by_you">Heu canviat el nom del grup de «%1$s» a «%2$s».</string>
<!-- %1$s will be replaced by the old group name, %2$s will be replaced by the new group name, %3$s will be replaced by name and address of the contact who did the action. -->
<string name="group_name_changed_by_other">Nom del grup \"%1$s\" canviat a \"%2$s\" per %3$s.</string>
<!-- %1$s will be replaced by the old channel name, %2$s will be replaced by the new channel name -->
<string name="channel_name_changed">S\'ha canviat el nom del canal de \"%1$s\" a \"%2$s\".</string>
<string name="group_image_changed_by_you">Heu canviat la imatge de grup.</string>
<!-- %1$s will be replaced by name of the contact who did the action -->
<string name="group_image_changed_by_other">Imatge de grup canviada per %1$s.</string>
<string name="channel_image_changed">Imatge del canal canviada.</string>
<string name="chat_description_changed_by_you">Has modificat la descripció del xat.</string>
<!-- %1$s will be replaced by name of the contact who did the action -->
<string name="chat_description_changed_by_other">Descripció del xat modificada per %1$s.</string>
@@ -1038,6 +1074,9 @@
<!-- first placeholder is the name of the chat -->
<string name="confirm_replace_draft">%1$s ja té un missatge d\'esborrany, voleu substituir-lo?</string>
<string name="mailto_link_could_not_be_decoded">No s\'ha pogut desxifrar l\'enllaç mailto: %1$s</string>
<string name="remove_quote">Elimina citació</string>
<string name="remove_attachment">Elimina adjunt</string>
<!-- notifications -->
<string name="notify_reply_button">Respondre</string>
<string name="notify_new_message">Missatge nou</string>
@@ -1054,6 +1093,10 @@
<string name="new_messages_body">Teniu missatges nous</string>
<string name="n_messages_in_m_chats">%1$d missatges en %2$d xats</string>
<!-- location streaming -->
<string name="location_streaming_notification_text">Estàs compartint la teva ubicació</string>
<string name="location_rationale">Per a compartir la teva ubicació en directe amb membres de xat, permet que Delta Chat utilitzi les teves dades d\'ubicació.\n\n Perquè la ubicació en directe funcioni sense interrupcions, s\'utilitzaran les dades d\'ubicació fins i tot quan l\'aplicació està tancada o no s\'està fent servir.</string>
<!-- permissions -->
<string name="perm_required_title">Es requereix permís</string>
<string name="perm_continue">Continua</string>
@@ -1076,7 +1119,7 @@
<string name="welcome_desktop">Us donem la benvinguda al Delta Chat</string>
<string name="global_menu_preferences_language_desktop">Trieu l\'idioma...</string>
<string name="global_menu_file_desktop">Arxiva</string>
<string name="global_menu_file_desktop">Aplicació</string>
<string name="global_menu_file_quit_desktop">Surt</string>
<string name="global_menu_edit_desktop">Modifica</string>
<string name="global_menu_edit_undo_desktop">Desfés</string>
@@ -1095,6 +1138,7 @@
<string name="global_menu_help_about_desktop">Quant al Delta Chat</string>
<string name="global_menu_file_open_desktop">Obre el Delta Chat</string>
<string name="global_menu_minimize_to_tray">Minimitza</string>
<string name="no_account_selected">Tria un perfil o crea un perfil nou</string>
<string name="no_chat_selected_suggestion_desktop">Trieu un xat o creeu-ne un de nou</string>
<string name="write_message_desktop">Escriu un missatge</string>
<string name="encryption_info_title_desktop">Informació de xifratge</string>
@@ -1116,8 +1160,16 @@
<!-- deprecated, used on desktop although there is not spell checker -->
<string name="no_spellcheck_suggestions_found">No s\'ha trobat cap suggeriment ortogràfic.</string>
<string name="show_window">Mostra la finestra</string>
<string name="data_found_other_installation_message">Els teus perfils d\'una altra instal·lació de Delta Chat no sóc accessibles - diferents instal·lacions emmagatzement la informació en llocs diferents.\n\nPer mantenir els perfils existents:\n1. Surt ara\n2. Obre l\'altre Delta Chat - si cal, reinstalal des de %1$s\n3. Fes una còpia de seguretat de cada perfil a \"Configuració/Xats\"\n4. Recarrega aquesta versió\n5. Restaura les còpies de seguretat utilitzant \"Ja tinc un perfil\"\n\nContinues sense els teus perfils anteriors?</string>
<!-- title of the "keybindings" dialog (for the keybindings names as such, where possible the normal command strings are used) -->
<string name="keybindings">Dreceres de teclat</string>
<!-- subtitle in the "keybindings" dialog -->
<string name="navigation_shortcut_section">Navegació</string>
<!-- subtitle in the "keybindings" dialog -->
<string name="message_input_shortcut_section">Introduir missatge</string>
<!-- subtitle in the "keybindings" dialog -->
<string name="message_selected_shortcut_section">Missatge seleccionat</string>
<string name="switch_between_chats">Canvia entre xats</string>
<string name="scroll_messages">Desplaça els missatges</string>
<!-- command to put the cursor to the search input field -->
@@ -1137,6 +1189,9 @@
<string name="a11y_message_context_menu_btn_label">Accions de missatge</string>
<string name="a11y_background_preview_label">Previsualització del fons de pantalla</string>
<string name="a11y_disappearing_messages_activated">S\'han activat els missatges efímers</string>
<!-- The placeholder will be replaced by a link, eg. "Open https://delta.chat". Used e.g. in for accessibility. -->
<string name="open_link">Obrir %1$s</string>
<!-- iOS specific strings, developers: please take care to remove strings that are no longer used! -->
<string name="stop_sharing_location">Deixa de compartir la ubicació</string>
<string name="a11y_voice_message_hint_ios">Després d\'enregistrar, feu doble toc per a enviar-lo. Per a descartar l\'enregistrament, fregueu amb dos dits.</string>
+1 -1
View File
@@ -673,7 +673,7 @@
<string name="welcome_chat_over_email">Sicherer dezentraler Chat</string>
<string name="scan_invitation_code">Einladungscode scannen</string>
<string name="login_title">Login</string>
<string name="login_advanced_hint">Für fortgeschrittene Benutzer:\n\nVerwende keine Adresse, die du bereits in einer anderen App nutzt.</string>
<string name="login_advanced_hint">Für fortgeschrittene Benutzer.\n\nVerwende keine Adresse, die du bereits in einer anderen App nutzt.</string>
<string name="login_inbox">Posteingang</string>
<string name="login_imap_login">IMAP-Anmeldename</string>
<string name="login_imap_server">IMAP-Server</string>
+11
View File
@@ -25,6 +25,8 @@
<string name="downloading">Deskargatzen...</string>
<string name="open_attachment">Erantsitakoa ireki</string>
<string name="join">Batu</string>
<string name="join_group">Sartu taldean</string>
<string name="join_channel">Sartu kanalean</string>
<string name="rejoin">Batu berriro</string>
<string name="delete">Ezabatu</string>
<string name="delete_for_me">Ezabatu niretzat</string>
@@ -72,6 +74,8 @@
<string name="mark_as_read">Ezarri irakurri gisa</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being read". In english, this could be "Read" (past tense of "to read"), in german, this could be "Gelesen". -->
<string name="mark_as_read_short">Markatu irakurrita</string>
<!-- Used as an menu entry or button -->
<string name="mark_as_unread">Markatu irakurri gabeko gisa</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being unread". In english, this could be "Unread" (past tense of "to unread"), in german, this could be "Ungelesen". -->
<string name="mark_as_unread_short">Irakurri gabe</string>
<!-- Placeholder text when something is loading -->
@@ -94,6 +98,7 @@
<string name="offline">Lineaz kanpo</string>
<!-- For the next view or as "continue". Should be as short as possible. -->
<string name="next">Hurrengoa</string>
<string name="warning">Abisua</string>
<string name="error">Errorea</string>
<string name="error_x">Errorea: %1$s</string>
<string name="no_app_to_handle_data">Ezin izan da datu mota hau kudeatzeko aplikaziorik.</string>
@@ -203,6 +208,8 @@
<string name="capture">Harrapatu</string>
<!-- Switch eg. from front to back camera -->
<string name="switch_camera">Aldatu kamera</string>
<!-- Toggle camera on/off eg. during an video call -->
<string name="toggle_camera">Aldatu kamera</string>
<string name="toggle_fullscreen">Aldatu pantaila osoa modua</string>
<string name="location">Kokalekua</string>
<string name="locations">Kokalekuak</string>
@@ -386,6 +393,8 @@
<string name="share_location_for_one_hour">Ordubetez</string>
<string name="share_location_for_two_hours">2 orduz</string>
<string name="share_location_for_six_hours">6 orduz</string>
<string name="share_location_for_24_hours">24 orduz</string>
<plurals name="ask_send_following_n_files_to">
<item quantity="one">Bidali hurrengo fitxategia %s-(e)ra?</item>
<item quantity="other">Bidali hurrengo %d fitxategiak %s(e)ra?</item>
@@ -402,6 +411,8 @@
<string name="answer_call">Erantzun</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Ukatu</string>
<!-- the action to end a call -->
<string name="end_call2">Amaitu deia</string>
<!-- Used in call bubbles, summaries, notifications -->
<string name="audio_call">Audio deia</string>
<!-- Used in call bubbles, summaries, notifications -->
File diff suppressed because it is too large Load Diff
+3
View File
@@ -450,6 +450,7 @@
<string name="call_answered_elsewhere">Chiamata risposta su un altro dispositivo</string>
<string name="call_requires_camera_permission">Per le videochiamate è necessaria l\'autorizzazione per l\'utilizzo della fotocamera.</string>
<string name="call_requires_mic_permission">Per le chiamate è necessaria l\'autorizzazione per l\'utilizzo del microfono.</string>
<string name="call_requires_connection">Impossibile avviare la chiamata. Assicurati che il tuo dispositivo disponga di una connessione a Internet e riprova.</string>
<!-- Used e.g. in a notifications to describe an ongoing call. "Call" is a noun here, in the meaning of "This is the call with ..." where the placeholder is replaced by the name of the contact. -->
<string name="call_with">Chiama con %1$s</string>
<!-- Use e.g. in a notifications during ringing. Placeholder is relaced by the name of the contact being called. -->
@@ -690,6 +691,7 @@
<string name="welcome_chat_over_email">Chat Decentralizzata Sicura</string>
<string name="scan_invitation_code">Scansiona Codice Invito</string>
<string name="login_title">Accedi</string>
<string name="login_advanced_hint">Questo login è riservato agli utenti avanzati.\n\nNon utilizzare un indirizzo che stai già usando in un\'altra app.</string>
<string name="login_inbox">In Arrivo</string>
<string name="login_imap_login">Nome Accesso IMAP</string>
<string name="login_imap_server">Server IMAP</string>
@@ -724,6 +726,7 @@
<string name="used_for_sending">Utilizzato per l\'invio</string>
<string name="hide_from_contacts">Nascondi dai Contatti</string>
<string name="hidden_from_contacts">Nascosto dai contatti</string>
<string name="enforce_e2ee">Applica Crittografia a Tutti i Ripetitori</string>
<!-- Hint for the list of relays -->
<string name="transport_list_hint">I messaggi vengono ricevuti su tutti i ripetitori.\n\n⚠️ Se modifichi qualcosa qui, assicurati che tutti i tuoi dispositivi abbiano almeno la versione 2.47.0. Altrimenti i dispositivi più vecchi potrebbero non ricevere i messaggi.</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
+6 -1
View File
@@ -944,7 +944,7 @@
<string name="welcome_desktop">Sveiki atvykę į Delta Chat</string>
<string name="global_menu_preferences_language_desktop">Pasirinkti kalbą...</string>
<string name="global_menu_file_desktop">Failas</string>
<string name="global_menu_file_quit_desktop">eiti</string>
<string name="global_menu_file_quit_desktop">jungti</string>
<string name="global_menu_edit_desktop">Taisyti</string>
<string name="global_menu_edit_undo_desktop">Atšaukti</string>
<string name="global_menu_edit_redo_desktop">Grąžinti</string>
@@ -956,6 +956,8 @@
<string name="global_menu_view_developer_desktop">Plėtotojas</string>
<string name="global_menu_view_developer_tools_desktop">Plėtotojo įrankiai</string>
<string name="global_menu_help_desktop">Žinynas</string>
<string name="delta_chat_homepage">Delta Chat internetinė svetainė</string>
<string name="contribute">Talkinti</string>
<string name="global_menu_help_report_desktop">Pranešti apie klaidą</string>
<string name="global_menu_help_about_desktop">Apie Delta Chat</string>
<string name="global_menu_file_open_desktop">Atverti Delta Chat</string>
@@ -990,6 +992,9 @@
<string name="a11y_message_context_menu_btn_label">Veiksmai su žinute</string>
<string name="a11y_background_preview_label">Fono peržiūra</string>
<string name="a11y_disappearing_messages_activated">Išnykstančios žinutės aktyvuotos</string>
<!-- The placeholder will be replaced by a link, eg. "Open https://delta.chat". Used e.g. in for accessibility. -->
<string name="open_link">Atverti %1$s</string>
<string name="a11y_connectivity_hint">Bakstelėkite du kartus norėdami pamatyti išsamesnę informaciją apie jungiamumą.</string>
<string name="login_error_no_internet_connection">Nėra interneto ryšio, nepavyko prisijungti.</string>
<string name="share_account_not_configured">Paskyra nėra sukonfigūruota.</string>
+3
View File
@@ -438,6 +438,7 @@
<string name="call_answered_elsewhere">Er is opgenomen op een ander apparaat</string>
<string name="call_requires_camera_permission">Cameratoegang is vereist om te kunnen videobellen</string>
<string name="call_requires_mic_permission">Microfoontoegang is vereist om te kunnen bellen</string>
<string name="call_requires_connection">De oproep kan niet worden gestart. Zorg dat je verbonden bent met het internet en probeer het opnieuw.</string>
<!-- Used e.g. in a notifications to describe an ongoing call. "Call" is a noun here, in the meaning of "This is the call with ..." where the placeholder is replaced by the name of the contact. -->
<string name="call_with">Bellen met %1$s</string>
<!-- Use e.g. in a notifications during ringing. Placeholder is relaced by the name of the contact being called. -->
@@ -672,6 +673,7 @@
<string name="welcome_chat_over_email">Veilige, gedecentraliseerde gesprekken</string>
<string name="scan_invitation_code">Uitnodigingscode scannen</string>
<string name="login_title">Inloggen</string>
<string name="login_advanced_hint">Deze inlogmethode is alleen voor ervaren gebruikers.\n\nGebruik geen adres dat je reeds in een andere app gebruikt.</string>
<string name="login_inbox">Postvak in</string>
<string name="login_imap_login">IMAP-inlognaam</string>
<string name="login_imap_server">IMAP-server</string>
@@ -706,6 +708,7 @@
<string name="used_for_sending">Wordt gebruikt voor versturen</string>
<string name="hide_from_contacts">Verbergen op contactpersonenlijst</string>
<string name="hidden_from_contacts">Verborgen op contactpersonenlijst</string>
<string name="enforce_e2ee">Versleuteling afdwingen op alle relays</string>
<!-- Hint for the list of relays -->
<string name="transport_list_hint">Berichten worden op alle relays ontvangen.\n\n⚠️ Als je hier iets wijzigt, doe dat dan alléén als ál je apparaten gebruikmaken van versie 2.47.0, anders kunnen er berichten ontbreken.</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
+16 -1
View File
@@ -228,6 +228,8 @@
<string name="capture">Zrób to</string>
<!-- Switch eg. from front to back camera -->
<string name="switch_camera">Przełącz kamerę</string>
<!-- Toggle camera on/off eg. during an video call -->
<string name="toggle_camera">Przełącz kamerę</string>
<string name="toggle_fullscreen">Przełącz na tryb pełnoekranowy</string>
<string name="location">Lokalizacja</string>
<string name="locations">Lokalizacje</string>
@@ -433,6 +435,8 @@
<string name="answer_call">Odbierz</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Odrzuć</string>
<!-- the action to end a call -->
<string name="end_call2">Zakończ połączenie</string>
<!-- Used in call bubbles, summaries, notifications -->
<string name="audio_call">Połączenie audio</string>
<!-- Used in call bubbles, summaries, notifications -->
@@ -456,7 +460,9 @@
<string name="missed_call">Połączenie anulowane</string>
<string name="already_in_call">Już w trakcie rozmowy</string>
<string name="call_answered_elsewhere">Połączenie odebrane na innym urządzeniu</string>
<string name="call_requires_camera_permission">Do połączeń wideo wymagane jest zezwolenie na korzystanie z kamery</string>
<string name="call_requires_mic_permission">Do połączeń wymagane jest zezwolenie na korzystanie z mikrofonu</string>
<string name="call_requires_connection">Nie można nawiązać połączenia. Upewnij się, że urządzenie ma połączenie z internetem i spróbuj ponownie.</string>
<!-- Used e.g. in a notifications to describe an ongoing call. "Call" is a noun here, in the meaning of "This is the call with ..." where the placeholder is replaced by the name of the contact. -->
<string name="call_with">Rozmowa z %1$s</string>
<!-- Use e.g. in a notifications during ringing. Placeholder is relaced by the name of the contact being called. -->
@@ -479,6 +485,13 @@
<item quantity="many">Usunąć %d wiadomości?</item>
<item quantity="other">Usunąć %d wiadomości?</item>
</plurals>
<!-- First placeholder gets replaced by the number of messages to forward. Second placeholder gets replaced by the name of the destination chat -->
<plurals name="ask_forward_messages">
<item quantity="one">Przekazać wiadomość do %2$s?</item>
<item quantity="few">Przekazać %1$d wiadomości do %2$s?</item>
<item quantity="many">Przekazać %1$d wiadomości do %2$s?</item>
<item quantity="other">Przekazać %1$d wiadomości do %2$s?</item>
</plurals>
<!-- deprecated, use ask_forward_messages -->
<string name="ask_forward">Przekazać wiadomości do użytkownika %1$s?</string>
<string name="ask_forward_multiple">Przekazać wiadomości do %1$d czatów?</string>
@@ -696,6 +709,7 @@
<string name="welcome_chat_over_email">Bezpieczny zdecentralizowany czat</string>
<string name="scan_invitation_code">Zeskanuj kod zaproszenia</string>
<string name="login_title">Zaloguj się</string>
<string name="login_advanced_hint">To logowanie jest przeznaczone dla zaawansowanych użytkowników.\n\nNie używaj adresu, którego używasz w innej aplikacji.</string>
<string name="login_inbox">Skrzynka odbiorcza</string>
<string name="login_imap_login">Nazwa użytkownika IMAP</string>
<string name="login_imap_server">Serwer IMAP</string>
@@ -730,6 +744,7 @@
<string name="used_for_sending">Służy do wysyłania</string>
<string name="hide_from_contacts">Ukryj w kontaktach</string>
<string name="hidden_from_contacts">Ukryty w kontaktach</string>
<string name="enforce_e2ee">Wymuś szyfrowanie dla wszystkich przekaźników</string>
<!-- Hint for the list of relays -->
<string name="transport_list_hint">Wiadomości są odbierane na wszystkich przekaźnikach.\n\n⚠️ Jeśli cokolwiek tutaj zmienisz, upewnij się, że wszystkie twoje urządzenia mają co najmniej wersję 2.47.0. W przeciwnym razie starsze urządzenia mogą nie odebrać wiadomości.</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
@@ -805,7 +820,7 @@
<!-- Title for the "Calls" notification setting. This is a plural noun, not a verb. -->
<string name="pref_calls">Połączenia</string>
<!-- Details about what the "Calls" notification setting does -->
<string name="pref_calls_explain">Pokaż ekran połączenia przy połączeniach przychodzących</string>
<string name="pref_calls_explain">Pokaż ekran połączenia dla zaakceptowanych kontaktów</string>
<string name="pref_notifications_show">Pokaż</string>
<string name="pref_notifications_priority">Priorytet</string>
<string name="pref_notifications_explain">Włącz powiadomienia systemowe o nowych wiadomościach</string>
+155
View File
@@ -25,8 +25,12 @@
<string name="downloading">Descarregando…</string>
<string name="open_attachment">Abrir anexo</string>
<string name="join">Entrar</string>
<string name="join_group">Entrar no Grupo</string>
<string name="join_channel">Entrar no Canal</string>
<string name="rejoin">Re-entrar</string>
<string name="delete">Apagar</string>
<string name="delete_for_me">Apagar para mim</string>
<string name="delete_for_everyone">Apagar para todos</string>
<string name="info">Informação</string>
<string name="update">Atualizar</string>
<string name="emoji">Emoji</string>
@@ -36,27 +40,44 @@
<string name="close_window">Fechar Janela</string>
<string name="forward">Encaminhar</string>
<string name="create">Criar</string>
<string name="reset">Voltar à configuração padrão</string>
<string name="later">Depois</string>
<!-- "Resend" means "Sending the selected message(s) again to the same chat". The string is used in a menu and should be as short as possible. Resending may be needed after failures or to repost old messages to new members. -->
<string name="resend">Re-enviar</string>
<string name="edited">Editado</string>
<string name="edit_message">Editar a mensagem</string>
<!-- Verb "to archive", as in "put a chat in the archive", not a noun "The Archive". -->
<string name="archive">Arquivar</string>
<!-- Verb "to unarchive", as in "remove a chat from the archive", opposite of the previous string -->
<string name="unarchive">Desarquivar</string>
<!-- Verb "to mute". used to mute a chat, so that it is silenced e.g. does not make noise or cause notifications -->
<string name="mute">Silenciar</string>
<!-- Adjective "smth is muted". used to describe a muted or silenced chat, that e.g. does not make noise or cause notifications -->
<string name="muted">Mudo</string>
<string name="ephemeral_messages">Mensagens efêmeras</string>
<string name="ephemeral_messages_hint">Essas configurações se aplicam a todos os participantes usando o Delta Chat. No entanto, eles podem copiar, salvar e encaminhar mensagens ou usar outros clientes de email.</string>
<string name="save">Salvar</string>
<string name="chat">Conversa</string>
<string name="media">Mídia</string>
<string name="apps_and_media">Aplicativos &amp; Mídias</string>
<string name="profile">Perfil</string>
<!-- deprecated -->
<string name="all_profiles">Todos os perfis</string>
<!-- deprecated -->
<string name="current_profile">Perfil atual</string>
<string name="main_menu">Menu principal</string>
<string name="start_chat">Começar conversa</string>
<string name="show_full_message">Mostrar mensagem completa...</string>
<!-- Stay short here, say ~16 characters. The source string could also be "All Read", maybe that hint can make translations easier :) -->
<string name="mark_all_as_read">Marcar tudo como lido</string>
<!-- Used as an menu entry or button -->
<string name="mark_as_read">Marcar como lido</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being read". In english, this could be "Read" (past tense of "to read"), in german, this could be "Gelesen". -->
<string name="mark_as_read_short">Lida</string>
<!-- Used as an menu entry or button -->
<string name="mark_as_unread">Marcar como não lido</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being unread". In english, this could be "Unread" (past tense of "to unread"), in german, this could be "Ungelesen". -->
<string name="mark_as_unread_short">Não lidas</string>
<!-- Placeholder text when something is loading -->
<string name="loading">Carregando...</string>
<string name="hide">Esconder</string>
@@ -77,11 +98,14 @@
<string name="offline">Desconectado</string>
<!-- For the next view or as "continue". Should be as short as possible. -->
<string name="next">Avançar</string>
<string name="warning">Aviso</string>
<string name="error">Erro</string>
<string name="error_x">Erro: %1$s</string>
<string name="no_app_to_handle_data">Não foi possível encontrar um app para abrir esse tipo de arquivo.</string>
<string name="no_browser_installed">Nenhum navegador instalado.</string>
<string name="file_not_found">Não foi possível encontrar %1$s.</string>
<!-- the placeholder will be replaced by the name of a file -->
<string name="cannot_save_file">Não é possível salvar %1$s.</string>
<string name="copied_to_clipboard">Salvo na área de transferência.</string>
<string name="contacts_headline">Contatos</string>
<string name="email_address">Endereço de email</string>
@@ -102,6 +126,14 @@
<!-- 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">Visto por último %1$s</string>
<string name="last_seen_unknown">Visto pela última vez: desconhecido</string>
<!-- Call duration as shown in the call bubbles. There is some risk to mix with the sending time, so the word "Duration" may be helpful. Might also be "Duration: %d minutes", "%d-minute duration", "%d-minute length" or "%d-minute call" -->
<plurals name="call_duration_minutes">
<item quantity="one">%d duração de um minuto</item>
<item quantity="many">%d minutos de duração</item>
<item quantity="other">%d minutos de duração</item>
</plurals>
<!-- Call duration as shown in the call bubbles in case the call is less than one minute. If the context is not clear enough, one may want to add the word "Duration". -->
<string name="call_duration_less_than_a_minute">Menos de 1 minuto</string>
<!-- Shown beside messages that are "N minutes old". Prefer short strings, or well-known abbreviations. -->
<plurals name="n_minutes">
<item quantity="one">%d min</item>
@@ -130,6 +162,11 @@
<item quantity="many">%d mensagens</item>
<item quantity="other">%d mensagens</item>
</plurals>
<plurals name="n_reactions">
<item quantity="one">%d reação</item>
<item quantity="many">%d reações</item>
<item quantity="other">%d reações</item>
</plurals>
<plurals name="n_members">
<item quantity="one">%d participantes</item>
<item quantity="many">%d membros</item>
@@ -146,6 +183,8 @@
<item quantity="many">%d selecionados</item>
<item quantity="other">%d selecionados</item>
</plurals>
<string name="selected_colon">Selecionado:</string>
<string name="selected">Selecionado</string>
<string name="self">Eu</string>
<string name="draft">Rascunho</string>
<string name="image">Imagem</string>
@@ -154,6 +193,7 @@
<string name="gif">Gif</string>
<!-- "Stickers" as known from other messengers; in some languages, the English "Sticker" is fine. -->
<string name="sticker">Sticker</string>
<string name="add_to_sticker_collection">Adicionar à coleção de adesivos</string>
<string name="images">Imagens</string>
<!-- a noun, used for "Music" and other "Audio" files -->
<string name="audio">Áudio</string>
@@ -170,6 +210,8 @@
<string name="capture">Capturar</string>
<!-- Switch eg. from front to back camera -->
<string name="switch_camera">Trocar câmera</string>
<!-- Toggle camera on/off eg. during an video call -->
<string name="toggle_camera">Alternar câmera</string>
<string name="toggle_fullscreen">Ativar modo tela cheia</string>
<string name="location">Localização</string>
<string name="locations">Localizações</string>
@@ -177,10 +219,22 @@
<string name="images_and_videos">Imagens e vídeos</string>
<string name="file">Arquivo</string>
<string name="files">Arquivos</string>
<!-- "Files" here means the "Files Selector App" or "Files Manager App" -->
<string name="choose_from_files">Escolher dos arquivos</string>
<string name="choose_from_gallery">Escolha na Galeria</string>
<!-- "App" is used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_app">App</string>
<!-- plural of "App"; used to present "Webxdc App" (https://webxdc.org) in a user friendly way. Please stay close to the original term and keep it short (it is used in menus with few screen space). -->
<string name="webxdc_apps">Apps</string>
<string name="home">Início</string>
<string name="games">Jogos</string>
<string name="tools">Ferramentas</string>
<string name="app_size">Tamanho</string>
<!-- Used as a headline for a date. E.g. in the details of a mini app in the app picker -->
<string name="app_date_published">Publicado</string>
<string name="add_to_chat">Adicionar à conversa</string>
<!-- short for "Browse through the App Picker/Store/Catalogue"; could also be translated as "Discover" or "Search" -->
<string name="browse">Navegar</string>
<string name="unknown">Desconhecido</string>
<string name="green">Verde</string>
@@ -204,8 +258,31 @@
<!-- menu labels (or icon, buttons...) -->
<string name="menu_new_contact">Novo contato</string>
<string name="menu_new_classic_contact">Adicionar contacto manualmente</string>
<string name="menu_new_chat">Nova conversa</string>
<string name="menu_new_group">Novo grupo</string>
<!-- "Chat" is a verb here, "Message to" would also fit. the string might be used in the "New Chat" screen above the contact list -->
<string name="chat_with">Conversa com...</string>
<string name="clone_chat">Clonar conversa</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel">Canal</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channels">Canais</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="new_channel">Novo canal</string>
<!-- consider keeping the term "channel" as in WhatsApp or Telegram -->
<string name="channel_name">Nome do canal</string>
<!-- The number of times a message is shown in channels. Use what other messengers are using here, smth. as "N-times seen" or "1 Aufruf" (german) work as well, where "1 impression" or "1 hit" may sound too formal and may be more marketing slang. -->
<plurals name="n_views">
<item quantity="one">%d visualização</item>
<item quantity="many">%d visualizações</item>
<item quantity="other">%d visualizações</item>
</plurals>
<string name="email">E-mail</string>
<!-- "New" as in "Create New Email"; shown together with "New Group" and "New Channel" -->
<string name="new_email">Novo e-mail</string>
<!-- the "Subject" of an email, use the term common in classic email apps -->
<string name="subject">Assunto</string>
<string name="menu_send">Enviar</string>
<string name="menu_toggle_keyboard">Ativar teclado de emojis</string>
<string name="menu_edit_group">Modificar grupo</string>
@@ -215,6 +292,8 @@
<string name="menu_unarchive_chat">Desarquivar conversa</string>
<string name="menu_add_attachment">Inserir anexo</string>
<string name="menu_leave_group">Sair do grupo</string>
<string name="menu_leave_channel">Sair do canal</string>
<string name="menu_leave_and_delete">Sair &amp; apagar para mim</string>
<string name="menu_delete_chat">Apagar conversa</string>
<!-- Command to delete all messages in a chat. The chat itself will not be deleted but will be empty afterwards, so make sure to be different from "Delete Chat" here. "Clear" is a verb here, "Empty Chat" would also be fine (eg. in German "Chat leeren") -->
<string name="clear_chat">Limpar bate-papo</string>
@@ -222,6 +301,10 @@
<string name="menu_delete_location">Apagar esta localização?</string>
<string name="menu_message_details">Detalhes da mensagem</string>
<string name="menu_copy_to_clipboard">Copiar para a área de transferência</string>
<string name="share_invite_link">Compartilhar link de convite</string>
<string name="invite_friends">Convidar amigos</string>
<!-- %1$s is replaced by the user's invitation link ("https://i.delta.chat/...") -->
<string name="invite_friends_text">Fale comigo no Delta Chat:\n%1$s</string>
<string name="menu_copy_selection_to_clipboard">Copiar seleção</string>
<string name="menu_copy_link_to_clipboard">Copiar link</string>
<string name="menu_copy_text_to_clipboard">Copiar texto</string>
@@ -235,8 +318,10 @@
<string name="menu_export_attachment">Exportar o anexo</string>
<string name="menu_export_attachments">Exportar anexos</string>
<string name="menu_all_media">Todos os arquivos de mídia</string>
<string name="all_apps_and_media">Todos os aplicativos &amp; Media</string>
<!-- Command to jump to the original message corresponding to a gallery image or document -->
<string name="show_in_chat">Mostrar na conversa</string>
<string name="show_app_in_chat">Mostrar aplicativo na conversa</string>
<string name="menu_share">Compartilhar</string>
<!-- Verb "to block sth." usually a mailing list or a contact, not a noun -->
<string name="block">Bloquear</string>
@@ -248,11 +333,17 @@
<string name="menu_scroll_to_bottom">Rolar até o fim</string>
<string name="menu_scroll_to_top">Rolar até o início</string>
<string name="menu_help">Ajuda</string>
<!-- use the same term for "Apps" as elsewhere -->
<string name="what_is_webxdc">Sobre os aplicativos</string>
<string name="privacy_policy">Política de privacidade</string>
<string name="menu_select_all">Selecionar tudo</string>
<string name="select_chat">Selecionar Chat</string>
<string name="select_more">Selecionar mais</string>
<string name="menu_edit_name">Modificar nome</string>
<!-- The placeholder will be replaced by the name the contact gave themself -->
<string name="edit_name_explain">Defina um apelido que será mostrado a você em vez de \"%1$s\". Deixe em branco para usar o nome escolhido pelo contato.</string>
<!-- The placeholder will be replaced by the name the contact gave themself -->
<string name="edit_name_placeholder">Apelido para \"%1$s\"</string>
<string name="menu_settings">Configurações</string>
<string name="menu_advanced">Avançado</string>
<string name="menu_view_profile">Ver perfil</string>
@@ -281,6 +372,7 @@
<string name="source_code">Código Fonte</string>
<!-- Menu item beside an app/chat that adds an icon to the system's home screen. If the user taps that icon, the app/chat is opened directly. -->
<string name="add_to_home_screen">Adicionar à Tela Inicial</string>
<string name="donate">Doação</string>
<string name="mute_for_one_hour">Silenciar por 1 hora</string>
<string name="mute_for_eight_hours">Silenciar por 8 horas</string>
<string name="mute_for_one_day">Silenciar por 1 dia</string>
@@ -294,6 +386,8 @@
<string name="share_location_for_one_hour">por 1 hora</string>
<string name="share_location_for_two_hours">por 2 horas</string>
<string name="share_location_for_six_hours">por 6 horas</string>
<string name="share_location_for_24_hours">Por 24 horas</string>
<plurals name="ask_send_following_n_files_to">
<item quantity="one">Enviar o seguinte arquivo para %s?</item>
<item quantity="many">Enviar os seguintes arquivos %d para %s?</item>
@@ -301,11 +395,54 @@
</plurals>
<string name="file_saved_to">Arquivo salvo em \"%1$s\".</string>
<!-- the action "to call someone", used as a tooltip for the "phone" icon. not: "the call" -->
<string name="start_call">Chamada</string>
<!-- Menu item to start an audio call; verbs as "start" are probably superfluous in many translations -->
<string name="start_audio_call">Chamada de áudio</string>
<!-- Menu item to start a video call; verbs as "start" are probably superfluous in many translations -->
<string name="start_video_call">Chamada de vídeo</string>
<!-- the action "to answer" or to "accept" or to "pick up" a call. not: "the answer" -->
<string name="answer_call">Resposta</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Não aceitar</string>
<!-- the action to end a call -->
<string name="end_call2">Encerrar chamada</string>
<!-- Used in call bubbles, summaries, notifications -->
<string name="audio_call">Chamada de áudio</string>
<!-- Used in call bubbles, summaries, notifications -->
<string name="video_call">Chamada de vídeo</string>
<string name="call_initializing">Inicializando...</string>
<string name="call_incoming">Chamada recebida</string>
<string name="call_ringing">Tocando...</string>
<string name="call_reconnecting">Reconectando...</string>
<string name="call_ended">Chamada encerrada</string>
<string name="call_failed">Falha na chamada</string>
<!-- deprecated, use "Audio call" instead -->
<string name="incoming_audio_call">Chamada de áudio recebida</string>
<string name="declined_call">Chamada recusada</string>
<string name="canceled_call">Chamada cancelada</string>
<string name="missed_call">Chamada perdida</string>
<string name="already_in_call">Já em uma chamada</string>
<!-- get confirmations -->
<!-- confirmation for leaving groups or channels. If a subject is needed, "Are you sure you want to leave the chat?" would work as well -->
<string name="ask_leave_group">Tem certeza de que quer sair?</string>
<plurals name="ask_delete_chat">
<item quantity="one">apagar %d conversa?</item>
<item quantity="many">apagar %d conversas?</item>
<item quantity="other">apagar %d conversas?</item>
</plurals>
<string name="ask_delete_message">Apagar esta mensagem?</string>
<plurals name="ask_delete_messages">
<item quantity="one">Apagar %d mensagem?</item>
<item quantity="many">Apagar %d mensagens?</item>
<item quantity="other">Apagar %d mensagens?</item>
</plurals>
<!-- First placeholder gets replaced by the number of messages to forward. Second placeholder gets replaced by the name of the destination chat -->
<plurals name="ask_forward_messages">
<item quantity="one">Encaminhar mensagem para %2$s?</item>
<item quantity="many">Encaminhar %1$d mensagens para %2$s?</item>
<item quantity="other">Encaminhar %1$d mensagens para %2$s?</item>
</plurals>
<!-- deprecated, use ask_forward_messages -->
<string name="ask_forward">Encaminhar mensagens para %1$s?</string>
<string name="ask_forward_multiple">Encaminhar mensagens para 1%1$d conversas?</string>
@@ -317,6 +454,8 @@
<string name="ask_start_chat_with">Conversar com %1$s?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_members">Remover %1$s do grupo?</string>
<!-- %1$s is replaced by a comma-separated list of names -->
<string name="ask_remove_from_channel">Remover %1$s do canal?</string>
<string name="open_url_confirmation">Você quer abrir este link?</string>
<!-- contact list -->
@@ -392,6 +531,7 @@
<!-- search -->
<string name="search">Procurar</string>
<string name="search_in_chat">Pesquisar na conversa</string>
<string name="search_explain">Procurar conversas, contatos e mensagens</string>
<string name="search_no_result_for_x">Nada relativo a \"%s\"</string>
<!-- create/edit groups, contact/group profile -->
@@ -468,6 +608,20 @@
<!-- 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 de %2$s usado</string>
<!-- welcome and login -->
<!-- Primary button on the welcome screen, allows to create an instant profile -->
<string name="onboarding_create_instant_account">Criar novo perfil</string>
<!-- Confirmation button on the instant onboarding screen -->
<string name="instant_onboarding_create">Concordar &amp; Criar perfil</string>
<!-- Hint about what happens when "Create Profile" button in pressed; the placeholder will be replaced by the group name -->
<string name="instant_onboarding_group_info">Crie um perfil para participar do grupo \"%1$s\".</string>
<!-- Hint about what happens when "Create Profile" button in pressed; the placeholder will be replaced by contact name -->
<string name="instant_onboarding_contact_info">Crie um perfil para conversa com %1$s.</string>
<!-- Question shown when another user's QR code is scanned from onboarding screen -->
<string name="instant_onboarding_confirm_contact">Crie um novo perfil e comece a conversar com %1$s?</string>
<!-- Question shown when group's/channel's QR code is scanned from onboarding screen -->
<string name="instant_onboarding_confirm_group">Criar um novo perfil e ingressar em \"%1$s\"?</string>
<string name="scan_invitation_code">Escanear Código De Convite</string>
<string name="login_title">Acessar</string>
<string name="login_inbox">Caixa de entrada</string>
@@ -797,6 +951,7 @@
<string name="global_menu_help_about_desktop">Sobre o Delta Chat</string>
<string name="global_menu_file_open_desktop">Abrir o Delta Chat </string>
<string name="global_menu_minimize_to_tray">Minimizar</string>
<string name="no_account_selected">seleccionar um perfil ou criar um novo perfil</string>
<string name="no_chat_selected_suggestion_desktop">Selecione uma conversa ou crie uma nova</string>
<string name="write_message_desktop">Escrever uma mensagem</string>
<string name="encryption_info_title_desktop">Informações da criptografia</string>
+6 -3
View File
@@ -462,6 +462,7 @@
<string name="call_answered_elsewhere">Звонок отвечен на другом устройстве</string>
<string name="call_requires_camera_permission">Для видеозвонков требуется разрешение на использование камеры</string>
<string name="call_requires_mic_permission">Для совершения звонков требуется разрешение на использование микрофона</string>
<string name="call_requires_connection">Не удалось начать вызов. Убедитесь, что ваше устройство подключено к интернету, и повторите попытку.</string>
<!-- Used e.g. in a notifications to describe an ongoing call. "Call" is a noun here, in the meaning of "This is the call with ..." where the placeholder is replaced by the name of the contact. -->
<string name="call_with">Звонок от %1$s</string>
<!-- Use e.g. in a notifications during ringing. Placeholder is relaced by the name of the contact being called. -->
@@ -487,9 +488,9 @@
<!-- First placeholder gets replaced by the number of messages to forward. Second placeholder gets replaced by the name of the destination chat -->
<plurals name="ask_forward_messages">
<item quantity="one">Переслать сообщение пользователю %2$s?</item>
<item quantity="few">Переслать %1$d сообщения пользователям %2$s?</item>
<item quantity="many">Переслать %1$d сообщения пользователям %2$s?</item>
<item quantity="other">Переслать %1$d сообщения пользователям %2$s?</item>
<item quantity="few">Переслать %1$d сообщения пользователю %2$s?</item>
<item quantity="many">Переслать %1$d сообщений пользователю %2$s?</item>
<item quantity="other">Переслать %1$d сообщений пользователю %2$s?</item>
</plurals>
<!-- deprecated, use ask_forward_messages -->
<string name="ask_forward">Переслать сообщения %1$s?</string>
@@ -708,6 +709,7 @@
<string name="welcome_chat_over_email">Безопасный децентрализованный чат</string>
<string name="scan_invitation_code">Сканировать код приглашения</string>
<string name="login_title">Войти</string>
<string name="login_advanced_hint">Этот способ входа предназначен для опытных пользователей.\n\nНе используйте адрес, который вы уже используете в другом приложении.</string>
<string name="login_inbox">Входящие</string>
<string name="login_imap_login">Имя пользователя IMAP</string>
<string name="login_imap_server">Сервер IMAP</string>
@@ -742,6 +744,7 @@
<string name="used_for_sending">Используется для отправки</string>
<string name="hide_from_contacts">Скрыть из контактов</string>
<string name="hidden_from_contacts">Скрыт из контактов</string>
<string name="enforce_e2ee">Принудительное шифрование для всех релеев</string>
<!-- Hint for the list of relays -->
<string name="transport_list_hint">Сообщения поступают на все релеи.\n\n⚠️ Если вы измените какие-либо настройки в этом разделе, убедитесь, что на всех ваших устройствах установлена версия Delta Chat не ниже 2.47.0. В противном случае старые версии могут не получать сообщения.</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
+4
View File
@@ -430,6 +430,7 @@
<string name="call_answered_elsewhere">Thirrje që ka marrë përgjigje në pajisje tjetër</string>
<string name="call_requires_camera_permission">Për thirrje me video janë të domosdoshme leje mbi kamerën</string>
<string name="call_requires_mic_permission">Leja mbi mikrofonin është e domosdoshme për thirrje</string>
<string name="call_requires_connection">Sniset dot thirrja. Sigurohuni se keni lidhje internet dhe riprovoni.</string>
<!-- Used e.g. in a notifications to describe an ongoing call. "Call" is a noun here, in the meaning of "This is the call with ..." where the placeholder is replaced by the name of the contact. -->
<string name="call_with">Thirrje me %1$s</string>
<!-- Use e.g. in a notifications during ringing. Placeholder is relaced by the name of the contact being called. -->
@@ -664,6 +665,7 @@
<string name="welcome_chat_over_email">Fjalosje e Decentralizuar, e Parrezik</string>
<string name="scan_invitation_code">Skanoni Kod Ftese</string>
<string name="login_title">Hyni</string>
<string name="login_advanced_hint">Kjo hyrje është për përdorues të thelluar.\n\nMos përdorni një adresë që e përdorni në një aplikacion tjetër.</string>
<string name="login_inbox">Të marrë</string>
<string name="login_imap_login">Emër Hyrjeje në IMAP</string>
<string name="login_imap_server">Shërbyes IMAP</string>
@@ -698,6 +700,7 @@
<string name="used_for_sending">E përdorur për dërgim</string>
<string name="hide_from_contacts">Fshihe prej Kontaktesh</string>
<string name="hidden_from_contacts">Fshehur prej kontakteve</string>
<string name="enforce_e2ee">Detyro Fshehtëzim për Krejt Reletë</string>
<!-- Hint for the list of relays -->
<string name="transport_list_hint">Mesazhet merren në të tëra reletë.\n\n⚠️ Nëse ndryshoni diçka këtu, sigurohuni se krejt pajisjet tuaja xhirojnë të paktën versionin 2.47.0. Përndryshe, pajisje më të vjetra mund të humbin mesazhe.</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
@@ -841,6 +844,7 @@
<string name="pref_show_emails_all">Krejt</string>
<string name="pref_experimental_features">Veçori Eksperimentale</string>
<string name="pref_experimental_features_explain">Këto veçori mund të jenë të paqëndrueshme dhe mund të ndryshohen, ose hiqen</string>
<string name="pref_on_demand_location_streaming">Dhënie Vendndodhjeje</string>
<string name="pref_background_default">Figurë parazgjedhje</string>
<string name="pref_background_default_color">Ngjyrë parazgjedhje</string>
<string name="pref_background_custom_image">Figurë vetjake</string>
+57
View File
@@ -25,6 +25,8 @@
<string name="downloading">Скачується…</string>
<string name="open_attachment">Відчинити прикріплене</string>
<string name="join">Долучитися</string>
<string name="join_group">Приєднатися до групи</string>
<string name="join_channel">Приєднатися до каналу</string>
<string name="rejoin">Передолучитися</string>
<string name="delete">Видалити</string>
<string name="delete_for_me">Видалити для мене</string>
@@ -72,6 +74,8 @@
<string name="mark_as_read">Позначити як прочитане</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being read". In english, this could be "Read" (past tense of "to read"), in german, this could be "Gelesen". -->
<string name="mark_as_read_short">Позначити прочитаним</string>
<!-- Used as an menu entry or button -->
<string name="mark_as_unread">Позначити непрочитаним</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being unread". In english, this could be "Unread" (past tense of "to unread"), in german, this could be "Ungelesen". -->
<string name="mark_as_unread_short">Непрочитане</string>
<!-- Placeholder text when something is loading -->
@@ -94,6 +98,7 @@
<string name="offline">Не в мережі</string>
<!-- For the next view or as "continue". Should be as short as possible. -->
<string name="next">Наступний</string>
<string name="warning">Попередження</string>
<string name="error">Помилка</string>
<string name="error_x">Помилка: %1$s</string>
<string name="no_app_to_handle_data">Додаток, що дозволяє обробляти цей тип даних, не знайдено.</string>
@@ -223,6 +228,8 @@
<string name="capture">Захопити</string>
<!-- Switch eg. from front to back camera -->
<string name="switch_camera">Перемкнути камеру</string>
<!-- Toggle camera on/off eg. during an video call -->
<string name="toggle_camera">Увімкнення/вимкнення камери</string>
<string name="toggle_fullscreen">Перейти в повноекранний режим</string>
<string name="location">Розташування</string>
<string name="locations">Розташування</string>
@@ -408,6 +415,8 @@
<string name="share_location_for_one_hour">на 1 годину</string>
<string name="share_location_for_two_hours">на 2 години</string>
<string name="share_location_for_six_hours">на 6 годин</string>
<string name="share_location_for_24_hours">На 24 години</string>
<plurals name="ask_send_following_n_files_to">
<item quantity="one">Надіслати цей файл → %s?</item>
<item quantity="few">Надіслати ці %d файли → %s?</item>
@@ -426,6 +435,8 @@
<string name="answer_call">Відповісти</string>
<!-- the action "to decline" a call, not: "the decline" -->
<string name="end_call">Відхилити</string>
<!-- the action to end a call -->
<string name="end_call2">Завершити дзвінок</string>
<!-- Used in call bubbles, summaries, notifications -->
<string name="audio_call">Аудіовиклик</string>
<!-- Used in call bubbles, summaries, notifications -->
@@ -448,6 +459,15 @@
<string name="canceled_call">Скасований дзвінок</string>
<string name="missed_call">Пропущений дзвінок</string>
<string name="already_in_call">Вже на зв\'язку</string>
<string name="call_answered_elsewhere">Дзвінок прийнято на іншому пристрої</string>
<string name="call_requires_camera_permission">Для відеодзвінків потрібен доступ до камери</string>
<string name="call_requires_mic_permission">Для здійснення дзвінків необхідний доступ до мікрофона</string>
<string name="call_requires_connection">Не вдається розпочати дзвінок. Переконайтеся, що на вашому пристрої є підключення до Інтернету, і спробуйте ще раз.</string>
<!-- Used e.g. in a notifications to describe an ongoing call. "Call" is a noun here, in the meaning of "This is the call with ..." where the placeholder is replaced by the name of the contact. -->
<string name="call_with">Зателефонувати до %1$s</string>
<!-- Use e.g. in a notifications during ringing. Placeholder is relaced by the name of the contact being called. -->
<string name="calling_person">Дзвонимо до %1$s…</string>
<!-- get confirmations -->
<!-- confirmation for leaving groups or channels. If a subject is needed, "Are you sure you want to leave the chat?" would work as well -->
<string name="ask_leave_group">Ви впевнені, що хочете піти?</string>
@@ -465,6 +485,13 @@
<item quantity="many">Видалити %d повідомлень?</item>
<item quantity="other">Видалити %d повідомлень?</item>
</plurals>
<!-- First placeholder gets replaced by the number of messages to forward. Second placeholder gets replaced by the name of the destination chat -->
<plurals name="ask_forward_messages">
<item quantity="one">Переслати повідомлення до %2$s?</item>
<item quantity="few">Переслати %1$d повідомлення до %2$s?</item>
<item quantity="many">Переслати %1$d повідомлень до %2$s?</item>
<item quantity="other">Переслати %1$d повідомлень до %2$s?</item>
</plurals>
<!-- deprecated, use ask_forward_messages -->
<string name="ask_forward">Переслати повідомлення %1$s?</string>
<string name="ask_forward_multiple">Переслати повідомлення в %1$d?</string>
@@ -560,8 +587,11 @@
<!-- search -->
<string name="search">Пошук</string>
<string name="search_in_chat">Шукати в чаті</string>
<!-- Placeholder will be replaced by the chat name -->
<string name="search_in">Пошук у %1$s</string>
<string name="search_files">Шукати файли</string>
<string name="search_explain">Пошук чатів, контактів і повідомлень</string>
<string name="search_result_for_x">Результати для \"%1$s\"</string>
<string name="search_no_result_for_x">Запит \"%s\" не дав результатів</string>
<!-- Adjective, as in "Show Unread Messages" -->
<string name="search_unread">Непрочитане</string>
@@ -574,6 +604,7 @@
<string name="group_create_button">Створити групу</string>
<!-- deprecated, use please_enter_chat_name -->
<string name="group_please_enter_group_name">Уведіть назву групі.</string>
<string name="please_enter_chat_name">Будь-ласка введіть ім\'я.</string>
<string name="group_add_members">Додати учасників</string>
<string name="group_self_not_in_group">Лише учасник групи має змогу виконати цю дію.</string>
<string name="profile_encryption">Шифрування</string>
@@ -678,6 +709,7 @@
<string name="welcome_chat_over_email">Безпечний децентралізований чат</string>
<string name="scan_invitation_code">Сканувати код запрошення</string>
<string name="login_title">Увійти</string>
<string name="login_advanced_hint">Цей спосіб входу призначений для досвідчених користувачів.\n\nНе використовуйте адресу, яку ви вже використовуєте в іншому додатку.</string>
<string name="login_inbox">Вхідні</string>
<string name="login_imap_login">IMAP-логін</string>
<string name="login_imap_server">IMAP-сервер</string>
@@ -712,6 +744,7 @@
<string name="used_for_sending">Використовується для відправки</string>
<string name="hide_from_contacts">Приховати від контактів</string>
<string name="hidden_from_contacts">Приховано від контактів</string>
<string name="enforce_e2ee">Застосувати політику шифрування для всіх ретрансляторів</string>
<!-- Hint for the list of relays -->
<string name="transport_list_hint">Повідомлення приймаються на всіх relay-серверах.\n\n⚠️ Якщо ви щось змінюєте тут, переконайтеся, що всі ваші пристрої працюють принаймні з версією 2.47.0. Інакше старіші пристрої можуть пропустити повідомлення.</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
@@ -811,6 +844,8 @@
<string name="pref_incognito_keyboard_explain">Вимагати вимкнення персоналізації підказок клавіатури</string>
<string name="pref_read_receipts">Отримувати сповіщення про прочитання</string>
<string name="pref_read_receipts_explain">Якщо сповіщення про прочитання відключені, Ви не будете бачити підтвердження прочитання повідомлень іншими користувачами.</string>
<!-- Title above a list of contacts who read a message -->
<string name="read_by">Прочитано</string>
<string name="pref_server">Сервер</string>
<string name="pref_encryption">Шифрування</string>
<string name="pref_manage_keys">Розпорядження ключами</string>
@@ -933,9 +968,12 @@
<string name="group_name_changed_by_you">Ви змінили назву групи з \"%1$s\" на \"%2$s\".</string>
<!-- %1$s will be replaced by the old group name, %2$s will be replaced by the new group name, %3$s will be replaced by name and address of the contact who did the action. -->
<string name="group_name_changed_by_other">%3$s змінив (-ла) назву групи з \"%1$s\" на \"%2$s\".</string>
<!-- %1$s will be replaced by the old channel name, %2$s will be replaced by the new channel name -->
<string name="channel_name_changed">Назва каналу змінена з «%1$s» на «%2$s».</string>
<string name="group_image_changed_by_you">Зображення групи змінено.</string>
<!-- %1$s will be replaced by name of the contact who did the action -->
<string name="group_image_changed_by_other">%1$s змінює зображення групи.</string>
<string name="channel_image_changed">Змінено зображення каналу.</string>
<string name="chat_description_changed_by_you">Ви змінили опис чату</string>
<!-- %1$s will be replaced by name of the contact who did the action -->
<string name="chat_description_changed_by_other">%1$s змінює опис чату.</string>
@@ -1072,6 +1110,9 @@
<!-- 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>
<string name="remove_quote">Видалити цитату</string>
<string name="remove_attachment">Видалити вкладення</string>
<!-- notifications -->
<string name="notify_reply_button">Відповісти</string>
<string name="notify_new_message">Нове повідомлення</string>
@@ -1088,6 +1129,10 @@
<string name="new_messages_body">У вас нові повідомлення</string>
<string name="n_messages_in_m_chats">%1$d повідомлень в %2$d чатах</string>
<!-- location streaming -->
<string name="location_streaming_notification_text">Ви ділитеся своїм місцеперебуванням</string>
<string name="location_rationale">Щоб ділитися своїм місцезнаходженням у реальному часі з учасниками чату, надайте Delta Chat доступ до даних про ваше місцезнаходження.\n\nДля безперервної роботи функції трансляції місцезнаходження дані про місцезнаходження використовуються навіть тоді, коли застосунок закрито або він не використовується.</string>
<!-- permissions -->
<string name="perm_required_title">Потрібен дозвіл</string>
<string name="perm_continue">Продовжити</string>
@@ -1129,6 +1174,7 @@
<string name="global_menu_help_about_desktop">Про Delta Chat</string>
<string name="global_menu_file_open_desktop">Відкрити Delta Chat</string>
<string name="global_menu_minimize_to_tray">Згорнути</string>
<string name="no_account_selected">Оберіть профіль або створіть новий профіль</string>
<string name="no_chat_selected_suggestion_desktop">Виберіть чат або створіть новий</string>
<string name="write_message_desktop">Напишіть повідомлення</string>
<string name="encryption_info_title_desktop">Інформація про шифрування</string>
@@ -1150,8 +1196,16 @@
<!-- deprecated, used on desktop although there is not spell checker -->
<string name="no_spellcheck_suggestions_found">Пропозицій щодо написання не знайдено.</string>
<string name="show_window">Показати вікно</string>
<string name="data_found_other_installation_message">Доступ до ваших профілів з іншої інсталяції Delta Chat неможливий — різні інсталяції зберігають свої дані в окремих місцях.\n\nЩоб зберегти існуючі профілі:\n1. Закрийте програму\n2. Відкрийте іншу версію Delta Chat — за потреби переінсталюйте її з %1$s\n3. Створіть резервну копію кожного профілю в меню «Налаштування/чати»\n4. Перезапустіть цю версію\n5. Відновіть резервні копії, вибравши «У мене вже є профіль»\n\nПродовжити без попередніх профілів?</string>
<!-- title of the "keybindings" dialog (for the keybindings names as such, where possible the normal command strings are used) -->
<string name="keybindings">Сполучення клавіш</string>
<!-- subtitle in the "keybindings" dialog -->
<string name="navigation_shortcut_section">Навігація</string>
<!-- subtitle in the "keybindings" dialog -->
<string name="message_input_shortcut_section">Введення повідомлення</string>
<!-- subtitle in the "keybindings" dialog -->
<string name="message_selected_shortcut_section">Обране повідомлення</string>
<string name="switch_between_chats">Переключитися між чатами</string>
<string name="scroll_messages">Прокрутити повідомлення</string>
<!-- command to put the cursor to the search input field -->
@@ -1171,6 +1225,9 @@
<string name="a11y_message_context_menu_btn_label">Дії з повідомленням</string>
<string name="a11y_background_preview_label">Попередній перегляд фону</string>
<string name="a11y_disappearing_messages_activated">Зникнення повідомлень увімкнено</string>
<!-- The placeholder will be replaced by a link, eg. "Open https://delta.chat". Used e.g. in for accessibility. -->
<string name="open_link">Відкрити %1$s</string>
<!-- iOS specific strings, developers: please take care to remove strings that are no longer used! -->
<string name="stop_sharing_location">Припинити надсилати геодані</string>
<string name="a11y_voice_message_hint_ios">Після запису двічі торкніться для відправки. Щоб скинути запис, проведіть двома пальцями.</string>
+3
View File
@@ -426,6 +426,7 @@
<string name="call_answered_elsewhere">通话已在其他设备上接听</string>
<string name="call_requires_camera_permission">视频通话需要摄像头权限</string>
<string name="call_requires_mic_permission">通话需要麦克风权限</string>
<string name="call_requires_connection">无法开始通话。请确保您的设备已连接到互联网,然后重试。</string>
<!-- Used e.g. in a notifications to describe an ongoing call. "Call" is a noun here, in the meaning of "This is the call with ..." where the placeholder is replaced by the name of the contact. -->
<string name="call_with">与 %1$s 的通话</string>
<!-- Use e.g. in a notifications during ringing. Placeholder is relaced by the name of the contact being called. -->
@@ -654,6 +655,7 @@
<string name="welcome_chat_over_email">安全的去中心化聊天</string>
<string name="scan_invitation_code">扫描邀请码</string>
<string name="login_title">登录</string>
<string name="login_advanced_hint">此登录适用于高级用户。\n\n请勿使用您在其他应用中已使用的地址。</string>
<string name="login_inbox">收件箱</string>
<string name="login_imap_login">IMAP 登录名</string>
<string name="login_imap_server">IMAP 服务器</string>
@@ -688,6 +690,7 @@
<string name="used_for_sending">用于发送</string>
<string name="hide_from_contacts">对联系人隐藏</string>
<string name="hidden_from_contacts">已对联系人隐藏</string>
<string name="enforce_e2ee">对所有中继强制加密</string>
<!-- Hint for the list of relays -->
<string name="transport_list_hint">在所有中继上均已收到消息。\n\n⚠️ 如果您在此处进行任何更改,请确保所有设备至少运行 2.47.0 版本。否则,较旧的设备可能会错过消息。</string>
<!-- shown if a QR code was scanned that can be used as a relay -->
+1 -1
View File
@@ -24,7 +24,7 @@
<dimen name="message_bubble_bottom_padding">6dp</dimen>
<dimen name="message_bubble_showmore_padding">8dp</dimen>
<dimen name="transparent_footer_padding">2dp</dimen>
<dimen name="media_bubble_remove_button_size">24dp</dimen>
<dimen name="media_bubble_remove_button_size">28dp</dimen>
<dimen name="media_bubble_edit_button_size">24dp</dimen>
<dimen name="media_bubble_default_dimens">210dp</dimen>
<dimen name="media_bubble_min_width">150dp</dimen>
+2 -2
View File
@@ -73,7 +73,7 @@
<!-- Used as an menu entry or button -->
<string name="mark_as_read">Mark as Read</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being read". In english, this could be "Read" (past tense of "to read"), in german, this could be "Gelesen". -->
<string name="mark_as_read_short">Mark Read</string>
<string name="mark_as_read_short">Read</string>
<!-- Used as an menu entry or button -->
<string name="mark_as_unread">Mark as Unread</string>
<!-- Used beside an icon with very few space. Shortest text for "Mark as being unread". In english, this could be "Unread" (past tense of "to unread"), in german, this could be "Ungelesen". -->
@@ -435,6 +435,7 @@
<string name="canceled_call">Canceled call</string>
<string name="missed_call">Missed call</string>
<string name="already_in_call">Already in a call</string>
<string name="call_back">Call back</string>
<string name="call_answered_elsewhere">Call answered on another device</string>
<string name="call_requires_camera_permission">Camera permission is required for video calls</string>
<string name="call_requires_mic_permission">Microphone permission is required for calls</string>
@@ -834,7 +835,6 @@
<string name="pref_background_btn_gallery">Select From Gallery</string>
<!-- deprecated -->
<string name="pref_imap_folder_warn_disable_defaults">If you change this option, make sure, your server and your other clients are configured accordingly.\n\nOtherwise things may not work at all.</string>
<string name="login_imap_folder">IMAP folder</string>
<!-- No need to be literal here, you can also use "Use Multiple Devices", "Support Multiple Devices" or other fitting terms. However, it should fit to the wording or your language at https://delta.chat/help -->
<string name="pref_multidevice">Multi-Device Mode</string>
<string name="pref_multidevice_explain">Synchronize your messages with your other devices. Automatically enabled on adding a second device</string>
+17
View File
@@ -129,6 +129,23 @@
<item name="android:background">@drawable/button_secondary_background</item>
</style>
<style name="WhiteButtonPrimary">
<item name="drawableTint">@color/def_primary</item>
<item name="android:textColor">@color/def_primary</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:paddingRight">8dp</item>
<item name="android:background">@drawable/white_button_bg</item>
</style>
<style name="WhiteButtonSecondary">
<item name="drawableTint">@color/white</item>
<item name="android:textColor">@color/white</item>
<item name="android:drawablePadding">5dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:paddingRight">8dp</item>
<item name="android:background">@drawable/white_button_secondary_background</item>
</style>
<style
name="PreferenceThemeOverlay.Fix"
parent="PreferenceThemeOverlay.v14.Material">