Compare commits

...

207 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 0ac9bea63c Merge remote-tracking branch 'upstream/main' 2026-05-26 17:04:57 +02:00
adb 2bb3ad546e Merge pull request #4442 from deltachat/prep-2.50.0
prepare 2.50.0
2026-05-26 17:03:45 +02:00
adbenitez 41a6b9a6c9 prepare 2.50.0 2026-05-26 16:00:53 +02:00
adb fb7c119afc Merge pull request #4441 from deltachat/adb/update-translations-26-05-2026
update translations
2026-05-26 16:00:15 +02:00
adbenitez 5910fc245a update translations 2026-05-26 15:53:39 +02:00
adb 7d8887eddd Merge pull request #4440 from deltachat/adb/improve-changelog-26-05-2026
update changelog
2026-05-26 15:33:32 +02:00
adbenitez 37471ae9fa update changelog 2026-05-26 15:32:13 +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 29a4a4712c Merge pull request #4439 from deltachat/adb/improve-conversationListFragment
Improve ConversationListFragment
2026-05-25 16:52:29 +02:00
adb 644bd1f594 Merge branch 'main' into adb/improve-conversationListFragment 2026-05-25 16:52:10 +02:00
adb ed6eca8920 Merge pull request #4431 from deltachat/adb/add-enforce-e2ee
add "Enforce e2ee" switch
2026-05-25 16:51:54 +02:00
adbenitez 47512f786e Improve ConversationListFragment
* remove unused queryFilter (always empty)
* add debugging information about the time DcContext.getChatlist() takes
2026-05-25 15:48:57 +02:00
adbenitez add51186e6 Merge remote-tracking branch 'upstream/main' 2026-05-25 15:35:22 +02:00
adb 52f63b28ba Merge branch 'main' into adb/add-enforce-e2ee 2026-05-24 00:11:32 +02:00
adb 889c9aab45 Merge pull request #4438 from deltachat/update-core-and-stuff-22-05-2026
Update core to 2.50.0
2026-05-24 00:10:36 +02:00
adbenitez de5940e709 update strings 2026-05-22 23:01:35 +02:00
adbenitez 6e184f735d update RPC bindings 2026-05-22 23:00:41 +02:00
adbenitez c33ff20d0a update changelog 2026-05-22 22:55:54 +02:00
adbenitez c67c3c8972 update deltachat-core-rust to 'chore(release): prepare for 2.50.0' of 'v2.50.0' 2026-05-22 22:30:13 +02:00
B. Petersen f98a5178b3 update advanced login hint 2026-05-22 17:55:27 +02:00
B. Petersen 6dfccdab84 update CHANGELOG 2026-05-22 17:52:34 +02:00
B. Petersen 40faa3d43e allow cancel without chaning encryption setting 2026-05-22 17:50:35 +02:00
B. Petersen 74f6853815 use correct flag from core 2026-05-22 17:32:58 +02:00
B. Petersen cef6817187 make clear, the 'encryption' setting affects all relays 2026-05-22 16:44:48 +02:00
B. Petersen 6f547dfb66 fix: hide 'encryption' switch if unexpanded 2026-05-22 16:41:10 +02:00
adbenitez d6e05f61ac add "Enforce e2ee" switch 2026-05-22 16:33:12 +02:00
wchen342 55649b0c4b Parse invitation link from search (#4432) 2026-05-15 18:10:32 +02:00
wchen342 4d9f8dd244 Update pinned commits; Remove separate PR comment workflow (#4434) 2026-05-15 17:42:18 +02:00
B. Petersen 3880da8a08 chore: "not supported by provider" is deprecated
the string will be removed at https://github.com/chatmail/core/pull/8247
2026-05-15 12:46:57 +02:00
adb 5e8666835d Merge pull request #4405 from deltachat/adb/issue-4401
remove legacy options
2026-05-13 22:44:19 +02:00
adbenitez 2f5619141e update changelog 2026-05-13 22:40:47 +02:00
adb 4a07d7fd9d Merge branch 'main' into adb/issue-4401 2026-05-13 21:31:17 +02:00
adb 1493583db7 Merge pull request #4426 from deltachat/adb/issue-4422
migrate to ViewPager2
2026-05-13 19:21:54 +02:00
adbenitez dc363a0281 Merge branch 'adb/issue-4422' of https://github.com/deltachat/deltachat-android into adb/issue-4422 2026-05-13 19:17:16 +02:00
adbenitez fcffe3922a fix createFragment 2026-05-13 19:17:00 +02:00
adb d546d8041f Merge branch 'main' into adb/issue-4422 2026-05-13 19:01:06 +02:00
adbenitez 267caf9b03 create a new QrShowFragment every time 2026-05-13 18:59:34 +02:00
wchen342 d55ad1f32a Call: Add notice when offline (#4425)
* Call: Add notice when offline

Co-authored-by: biörn <r10s@b44t.com>
2026-05-13 16:04:03 +02:00
B. Petersen be07043b47 prefer 'audio' over 'switch speaker', which is more correct, others are using that as well, and the translations are there already 2026-05-12 15:19:36 +02:00
adb cc3e6bcd9d Merge branch 'main' into adb/issue-4401 2026-05-12 15:18:17 +02:00
biörn 43654fdadb feat: tweak forward confirmation (#4427)
* cleanup: remove dead code

the special handling of single-chats was to show the email address in the past,
it was introduced at https://github.com/deltachat/deltachat-android/pull/1049 .
as we do no longer show the email address, this superfluous code now.

* feat: show impact of forwarding in confirmation dialog

* feat: show 'Forward' verb on button

* apply spotless
2026-05-12 13:00:42 +00:00
adbenitez 774add2380 update changelog 2026-05-11 23:52:12 +02:00
adbenitez 1133a6e624 migrate AllMediaActivity to ViewPager2 2026-05-11 23:43:17 +02:00
adbenitez 0aad1b3d76 migrate QrActivity to ViewPager2 2026-05-11 22:34:07 +02:00
adbenitez b58a9d0bab apply spotless 2026-05-11 17:58:47 +02:00
adbenitez 54a74a8586 migrate to ViewPager2 in MediaPreviewActivity 2026-05-11 17:48:19 +02:00
adb f25474947b Merge pull request #4421 from deltachat/adb/issue-4418
move requestPinShortcut to background
2026-05-08 17:14:10 +02:00
adbenitez 9ebd8e4a37 fix typos and other bugs 2026-05-08 17:09:01 +02:00
adbenitez 975ad2e149 wrap with try-catch 2026-05-08 17:01:47 +02:00
adbenitez e6350aaec2 move requestPinShortcut to background 2026-05-08 16:18:36 +02:00
B. Petersen d373537d6d avatars can be ignored by talkback, the name is in the title 2026-05-07 21:44:11 +02:00
B. Petersen 960581e5f2 make call buttons translatable 2026-05-07 21:44:11 +02:00
B. Petersen fccf8f402e use same theme colors as in camera, editor 2026-05-07 21:44:11 +02:00
B. Petersen e0459978f7 feat: use consistent colors and contrast 2026-05-07 21:44:11 +02:00
wchen342 72bd7376ca Make it possible to answer incoming calls from messages (#4415) 2026-05-07 16:29:27 +00:00
adb 94fbdcde05 Merge pull request #4408 from deltachat/adb/issue-4152
add a setting to enable/disable calls notification
2026-05-07 18:19:27 +02:00
wchen342 89d77e7638 Call: camera/audio permission related fixes (#4412)
* Call: camera/permission related fixes

Defer camera initialization until actually needed.

Fix missing camera permission which blocks call from initializing correctly.

Add dialog when audio permission is not granted.

Add redirection dialog when permissions are permanently denied.
2026-05-07 13:55:46 +02:00
adb d9edee117f Merge branch 'main' into adb/issue-4152 2026-05-06 15:04:57 +02:00
adbenitez 6cfdb87924 Merge branch 'adb/issue-4152' of https://github.com/deltachat/deltachat-android into adb/issue-4152 2026-05-06 15:04:40 +02:00
adbenitez 613940577c update changelog 2026-05-06 15:04:30 +02:00
B. Petersen 43a3e21495 add some context for translators, do not introduce another prefix 2026-05-06 15:03:46 +02:00
adb b5375b8c0e Merge branch 'main' into adb/issue-4152 2026-05-06 15:02:40 +02:00
B. Petersen 39aec04fea remove unhelpful channel description 2026-05-06 15:01:53 +02:00
B. Petersen 94e2c8dbed feat: simplify location streaming wording 2026-05-06 15:01:53 +02:00
adbenitez da45c7bc1c apply spotless 2026-05-06 15:01:51 +02:00
adbenitez d180704d52 allow to disable call notifications 2026-05-06 14:59:39 +02:00
B. Petersen 653c8688b7 mark deprecated strings as such 2026-05-06 14:42:49 +02:00
B. Petersen 9a382a4948 actually explain what the 'calls notification' option does 2026-05-06 14:42:49 +02:00
adb 74e14bd0ea Merge branch 'main' into adb/issue-4401 2026-05-05 15:12:03 +02:00
adbenitez 844231e5e7 Merge branch 'adb/issue-4401' of https://github.com/deltachat/deltachat-android into adb/issue-4401 2026-05-04 22:05:52 +02:00
adbenitez 0ccf0a9309 apply spotless 2026-05-04 22:05:42 +02:00
adb a57122a902 Merge branch 'main' into adb/issue-4401 2026-05-04 20:44:44 +02:00
adbenitez 464cba299a remove legacy options 2026-05-04 20:41:32 +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
160 changed files with 8221 additions and 2400 deletions
-26
View File
@@ -1,26 +0,0 @@
name: add artifact links to pull request
on:
workflow_run:
workflows: ["Upload Preview APK"]
types: [completed]
permissions: {}
jobs:
artifacts-url-comments:
name: add artifact links to pull request
permissions:
pull-requests: write
actions: read
contents: read
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: add artifact links to pull request
uses: tonyhallett/artifacts-url-comments@0965ff1a7ae03c5c1644d3c30f956effea4e05ef # v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
prefix: "**To test the changes in this pull request, install this apk:**"
format: "[📦 {name}]({url})"
addTo: pull
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
persist-credentials: false
@@ -25,7 +25,7 @@ jobs:
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
ndk-version: "r29"
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
@@ -37,7 +37,7 @@ jobs:
- name: Cache compiled core
id: cache-core
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
jni/arm64-v8a
+13 -3
View File
@@ -18,14 +18,16 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-java@v5
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 17
distribution: temurin
- uses: actions/cache@v4
- name: Restore Gradle cache
id: gradle-cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
@@ -37,3 +39,11 @@ jobs:
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Check formatting
run: ./gradlew spotlessCheck
- name: Save Gradle cache
if: github.event_name == 'push' && steps.gradle-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+27 -7
View File
@@ -12,21 +12,24 @@ jobs:
build:
name: Upload Preview APK
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
persist-credentials: false
- name: Validate Fastlane Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2
uses: ashutoshgngwr/validate-fastlane-supply-metadata@c8857fdbbd3e00f9a5cbe8604bcecfa95ce8fef8 # v2.1.0
- uses: actions/setup-java@v5
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 17
distribution: 'temurin'
- uses: actions/cache@v4
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
@@ -42,7 +45,7 @@ jobs:
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
ndk-version: "r29"
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
@@ -54,7 +57,7 @@ jobs:
- name: Restore compiled core
id: core-cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
jni/arm64-v8a
@@ -73,7 +76,24 @@ jobs:
run: ./gradlew --no-daemon -PABI_FILTER=arm64-v8a assembleFossDebug
- name: Upload APK
uses: actions/upload-artifact@v4
id: upload
if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: app-preview.apk
path: 'build/outputs/apk/foss/debug/*.apk'
- name: Add artifact links to PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
with:
script: |
const url = process.env.ARTIFACT_URL;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `**To test the changes in this pull request, install this apk:**\n\n[📦 app-preview.apk](${url})`,
});
+6 -6
View File
@@ -23,18 +23,18 @@ jobs:
name: Upload Release APK
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
working-directory: jni/deltachat-core-rust
- uses: actions/setup-java@v3
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 17
distribution: 'temurin'
- uses: android-actions/setup-android@v3
- uses: nttld/setup-ndk@v1
- uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
@@ -63,7 +63,7 @@ jobs:
mv build/outputs/mapping/gplayRelease/mapping.txt build/outputs/mapping/fossRelease/mapping-gplay.txt
- name: Release on GitHub
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
token: "${{ secrets.GITHUB_TOKEN }}"
body: '[<img src="store/get-it-on-gplay.png" alt="Get it on Google Play" height="48">](https://play.google.com/store/apps/details?id=com.github.arcanechat) [<img src="store/get-it-on-fdroid.png" alt="Get it on F-Droid" height="48">](https://f-droid.org/packages/chat.delta.lite) [<img src="store/get-it-on-github.png" alt="Get it on GitHub" height="48">](https://github.com/ArcaneChat/android/releases/latest/download/ArcaneChat-gplay.apk)'
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+5 -5
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`.
+57
View File
@@ -2,20 +2,77 @@
## Unreleased
* Allow to select multiple files for sending
* Add notifications for missed calls
* Video call preview now accurately shows what is sent to remote
* Fix: properly hide draft attachment during in-chat search
* Fix: close mini-apps and chats if they are deleted
* Fix: cancel in-chat search when back is pressed, instead of directly returning to chatlist
## v2.53.0
2026-06
* Use message style notifications for longer message previews
* Remove notification after audio playback ends
* Fix: do not allow blocked contacts to use our invite links
* Fix sending mini-app that was used/prepared before sending
* Some more small fixes and updated translations
* Update to core 2.53.0
## v2.52.0
2026-06
* Fix: avoid crashes in Media preview sometimes
* Fix: Incorrect total time when attaching audio files as draft
* Fix: Audio files in draft showing total time from wrong file
* Fix: Update the channel title after joining if the QR code had an outdated title
* Voice recording will be automatically saved as draft when interrupted
* Update to core 2.52.0
## v2.51.0
2026-06
* Better incoming call system integration
* Calls are not experimental anymore and don't need to be manually enabled
* Calls can be answered by tapping messages
* Notify the user when they try to make a call while the device is offline
* Channels are no longer experimental and are available by default
* Display a permanent notification when doing location streaming and get rid of dangerous "Access Location in Background" permission
* Autoplay all voice messages in a chat
* Allow to share location for 24 hours
* Allow mini-apps to play audio without user interaction
* Allow to paste and open invitation links from search
* Mark chats as unread (long tap a chat and select the corresponding option from the three-dot-menu)
* Add "Mark all as read" option to profile menu in the profile switcher
* Fix process of upgrading from a very old version of the app
* Show more recent added stickers at the top of the sticker picker
* Allow to open links in messages via actions in TalkBack menu
* Allow to open map if user clicks "Location streaming enabled" system message
* Allow to disable incoming calls notifications
* Add an option to process unencrypted messages; by default, only encrypted messages can be sent or received
* Fix: do not accidentally set draft in chats that don't allow sending messages
* Fix swipe navigation between tabs in RTL languages
* Remove "Move to DeltaChat folder", in case you are using the option, a device message shows how to proceed
* Remove "Only fetch from DeltaChat folder" option, the functionality is preserved for existing profiles
* Remove "Delete Messages from Server" option, this is now up to the server:
Chatmail handles that automatically, classic email servers used as relay often have lots of storage or options themselves
* Remove "Show Email" options, all messages are shown by default, shared usage of email account is not supported
* Allow otherwise invalid TLS connections if the key is unchanged
* Adapt quota warning to automatic cleanup.
* Don't show non-delivery-notfications in broadcast channels
* Resend the last 10 messages to new broadcast channel member
* Enable PQC (Post-Quantum Cryptography) support. We do not generate PQC keys yet, this step is needed for forward compatibility
* Improve avatar quality
* Add new webxdc.isAppSender and webxdc.isBroadcast APIs for mini-apps
* Fix: avoid invalid empty "~" notifications when some peer is streaming location
* Fix: Improve detection of stickers
* Fix text direction issues for RTL languages in "Show full message" view
* Fix: Reconnect when removing a relay
* Fix: Location streaming now works correctly with multiple accounts
* Fix debouncing in chatlist and search
* Fix sharing contact across profiles
* Fix: Reset scroll location after switching account
* Update to core 2.51.0
## v2.49.0
2026-04
+2 -2
View File
@@ -34,8 +34,8 @@ android {
useLibrary 'org.apache.http.legacy'
defaultConfig {
versionCode 30000742
versionName "2.49.0"
versionCode 30000746
versionName "2.53.0"
applicationId "chat.delta.lite"
multiDexEnabled true
+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"
-6
View File
@@ -1643,12 +1643,6 @@ JNIEXPORT void Java_com_b44t_messenger_DcMsg_setHtml(JNIEnv *env, jobject obj, j
}
JNIEXPORT void Java_com_b44t_messenger_DcMsg_forceSticker(JNIEnv *env, jobject obj)
{
dc_msg_force_sticker(get_dc_msg(env, obj));
}
JNIEXPORT jstring Java_com_b44t_messenger_DcMsg_getPOILocation(JNIEnv *env, jobject obj)
{
char* temp = dc_msg_get_poi_location(get_dc_msg(env, obj));
+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"
+216 -13
View File
@@ -29,6 +29,21 @@
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
@@ -628,6 +643,197 @@ but more than 150 is not recommended.</p>
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1425,23 +1631,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+213 -14
View File
@@ -29,6 +29,21 @@
<li><a href="#wie-viele-mitglieder-können-in-einer-einzelnen-gruppe-sein">Wie viele Mitglieder können in einer einzelnen Gruppe sein?</a></li>
</ul>
</li>
<li><a href="#channels">Kanäle</a>
<ul>
<li><a href="#einem-kanal-beitreten">Einem Kanal beitreten</a></li>
<li><a href="#einen-kanal-erstellen">Einen Kanal erstellen</a></li>
<li><a href="#wie-viele-empfänger-kann-ein-kanal-haben">Wie viele Empfänger kann ein Kanal haben?</a></li>
</ul>
</li>
<li><a href="#calls">Anrufe</a>
<ul>
<li><a href="#jemanden-anrufen">Jemanden anrufen</a></li>
<li><a href="#einen-anruf-annehmen-oder-ablehnen">Einen Anruf annehmen oder ablehnen</a></li>
<li><a href="#während-des-anrufs">Während des Anrufs</a></li>
<li><a href="#verpasste-anrufe-und-benachrichtigungen">Verpasste Anrufe und Benachrichtigungen</a></li>
</ul>
</li>
<li><a href="#webxdc">In-Chat-Apps</a>
<ul>
<li><a href="#wo-bekomme-ich-in-chat-apps">Wo bekomme ich In-Chat-Apps?</a></li>
@@ -584,6 +599,197 @@ aber mehr als 150 sind nicht empfohlen.</p>
<p>Wenn Gruppen größer werden, können sie sozial instabil werden und benötigen möglicherweise eine Hierarchie - und Delta Chat ist ein privater Messenger für Chats mit <a href="#groups">gleichen Rechten</a>. Vgl. <a href="https://de.wikipedia.org/wiki/Dunbar-Zahl">Dunbar-Zahl</a>.</p>
<h2 id="channels">
Kanäle <a href="#channels" class="anchor"></a>
</h2>
<p>Kanäle dienen der Verbreitung von Nachrichten an viele Empfänger.</p>
<h3 id="einem-kanal-beitreten">
Einem Kanal beitreten <a href="#einem-kanal-beitreten" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="einen-kanal-erstellen">
Einen Kanal erstellen <a href="#einen-kanal-erstellen" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="wie-viele-empfänger-kann-ein-kanal-haben">
Wie viele Empfänger kann ein Kanal haben? <a href="#wie-viele-empfänger-kann-ein-kanal-haben" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Anrufe <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="jemanden-anrufen">
Jemanden anrufen <a href="#jemanden-anrufen" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="einen-anruf-annehmen-oder-ablehnen">
Einen Anruf annehmen oder ablehnen <a href="#einen-anruf-annehmen-oder-ablehnen" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="während-des-anrufs">
Während des Anrufs <a href="#während-des-anrufs" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="verpasste-anrufe-und-benachrichtigungen">
Verpasste Anrufe und Benachrichtigungen <a href="#verpasste-anrufe-und-benachrichtigungen" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1329,23 +1535,16 @@ im Falle einer Beschlagnahmung des Geräts nicht ohne Weiteres identifiziert wer
</h3>
<p>Das verwendete <a href="#relays">Relay</a> muss Ihre IP-Adresse kennen,
sowie manchmal auch die Geräte Ihrer Kontakte, wenn Sie einen <a href="#experiments">Anruf</a> tätigen
oder gemeinsam <a href="#webxdc">Apps</a> verwenden.</p>
<p>Die verwendeten <a href="#relays">Relays</a> müssen deine IP-Adresse kennen,
sowie manchmal auch die Geräte deiner Kontakte, wenn du einen <a href="#calls">Anruf</a> tätigst
oder ihr gemeinsam <a href="#webxdc">Apps</a> verwendet.</p>
<p>IP-Adressen sind für Verbindungen und für Effizienz erforderlich.
Sie werden weder gespeichert noch offengelegt.
Beachten Sie, dass die IP-Adresse
nicht mit einer Adresse, die Sie einem Lieferdienst geben, vergleichbar ist -
sondern viel gröber ist und oft nur die Region oder das Land angibt.</p>
Sie werden von Delta Chat weder gespeichert noch offengelegt.
IP-Adressen sind nicht mit einer Adresse, die du einem Lieferdienst gibst, vergleichbar - sondern viel gröber und oft nur die Stadt oder die Region beschreibend.</p>
<p>Da dies die Standardfunktion des Internets und anderer Messenger ist,
bieten wir hier keine Optionen an und stellen auch keine Fragen im Voraus.</p>
<p>Wenn Sie Ihre IP-Adresse als Sicherheits- oder Datenschutzrisiko betrachten,
empfehlen wir Ihnen, ein VPN in Kombination mit dem System-Lockdown-Modus zu verwenden.
Alle einzelnen Apps auf Ihrem System nach IP-Optionen abzusuchen wird nicht zufriedenstellen sein;
beispielsweise legt das Antippen eines Links IP-Adressen gegenüber unbekannten Parteien offen und stellt hier das weitaus größere Risiko dar.</p>
<p>Wenn du deine IP-Adresse als Risiko betrachtest, empfehlen wir, ein VPN für das gesamte System zu verwenden.
Einstellungen auf App-Ebene hinterlassen Lücken überall im System. Wenn man beispielsweise auf einen Link tippt, können IP-Adressen an Unbekannte weitergegeben werden, was bei weitem das größere Risiko darstellt</p>
<h3 id="sealedsender">
+216 -13
View File
@@ -29,6 +29,21 @@
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
@@ -627,6 +642,197 @@ but more than 150 is not recommended.</p>
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1425,23 +1631,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
File diff suppressed because it is too large Load Diff
+218 -18
View File
@@ -29,6 +29,21 @@
<li><a href="#zenbat-kide-izan-ditzake-talde-batek">Zenbat kide izan ditzake talde batek?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">Txat barruko aplikazioak</a>
<ul>
<li><a href="#non-lortu-ditzaket-txat-barruko-aplikazioak">Non lortu ditzaket txat barruko aplikazioak?</a></li>
@@ -624,6 +639,197 @@ baina ez da komeni 150 baino gehiago izatea.</p>
Delta Chat <a href="#groups">eskubide berberekin</a> txateatzeko aukera ematen duen mezularitza pribatuko zerbitzu bat da.
Informazio gehiago nahi baduzu, kontsultatu <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbarren zenbakia</a>.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1431,26 +1637,20 @@ txat-kontaktuak ezin izango dira erraz identifikatu.</p>
</h3>
<p>Erabiltzen duzun <a href="#relays">erreleak</a> zure IP helbidea jakin behar du,
eta, batzuetan, baita zure kontaktuen gailuena ere; adibidez,
kontaktu horiekin <a href="#experiments">deiak</a> egiten badituzu edo
<a href="#webxdc">aplikazioak</a> erabiltzen badituzu.</p>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP helbideak ezinbestekoak dira konektibitaterako eta eraginkortasunerako.
Ez dira gordetzen, ez argitara ematen.
Kontuan izan IP helbidea ez dela mezularitzako zerbitzuei-eta eman ohi zaiena
bezalako helbide zehatz bat; askoz orokorragoa da, eta askotan eskualdea edo
herrialdea baizik ez du identifikatzen.</p>
<p>IP Addresses are needed for connectivity and efficiency.
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>Internetek eta beste mezularitza-aplikazioek horrela funtzionatzen dutenez,
ez dugu horren gaineko bestelako aukerarik eskaintzen, ez galderarik egiten.</p>
<p>Uste baduzu zure IP helbidea jakiteak zure segurtasuna edo pribatutasuna
arriskuan jar ditzakeela, zera gomendatuko genizuke, VPN bat erabiltzea,
sistema blokeatzeko moduarekin batera. Zure sistemako aplikazio guztietan
alternatibak bilatzeak segurtasun-zuloak utziko ditu; adibidez, esteka batean klik egindakoan,
IP helbideak agerian geratzen zaizkie hirugarren ezezagunei, eta
hori askoz arriskutsuagoa da.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+216 -13
View File
@@ -29,6 +29,21 @@
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
@@ -547,6 +562,197 @@ but more than 150 is not recommended.</p>
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1338,23 +1544,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+216 -13
View File
@@ -29,6 +29,21 @@
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
@@ -627,6 +642,197 @@ but more than 150 is not recommended.</p>
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1425,23 +1631,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+217 -15
View File
@@ -29,6 +29,21 @@
<li><a href="#quanti-membri-possono-partecipare-a-un-singolo-gruppo">Quanti membri possono partecipare a un singolo gruppo?</a></li>
</ul>
</li>
<li><a href="#canali">Canali</a>
<ul>
<li><a href="#iscriversi-a-un-canale">Iscriversi a un canale</a></li>
<li><a href="#creare-un-canale">Creare un canale</a></li>
<li><a href="#quanti-iscritti-può-avere-un-canale">Quanti iscritti può avere un canale?</a></li>
</ul>
</li>
<li><a href="#calls">Chiamate</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">Apps in chat</a>
<ul>
<li><a href="#dove-posso-trovare-le-apps-in-chat">Dove posso trovare le apps in chat?</a></li>
@@ -620,6 +635,196 @@ ma non è consigliabile superare i 150.</p>
dove Delta Chat è un servizio di messaggistica privato per chattare con <a href="#groups">uguali diritti</a>.
Vedi <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">numero di Dunbar</a> per ulteriori approfondimenti.</p>
<h2 id="canali">
Canali <a href="#canali" class="anchor"></a>
</h2>
<p>I canali sono uno strumento da uno a molti per la trasmissione di messaggi.</p>
<h3 id="iscriversi-a-un-canale">
Iscriversi a un canale <a href="#iscriversi-a-un-canale" class="anchor"></a>
</h3>
<ul>
<li>Scansiona il <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>Codice QR</strong>
oppure tocca il <strong>collegamento di invito</strong> che hai ricevuto dal proprietario del canale.</li>
</ul>
<p>Ecco fatto!
Riceverai alcuni dei messaggi dalla cronologia del canale
e, da quel momento in poi, tutti i nuovi messaggi dal canale.</p>
<p><strong>Non preoccuparti,</strong> se non accade immediatamente.
Non appena il proprietario del canale si connetterà, la tua richiesta di iscrizione verrà elaborata.</p>
<p>Poiché tutti i Canali di Delta Chat sono privati e decentralizzati,
non esiste una funzione di ricerca pubblica.</p>
<p>Gli altri iscritti al canale non vedranno che ti sei iscritto e non potranno inviarti messaggi.
Il proprietario del canale, tuttavia, potrà inviarti messaggi.
Inoltre, vedrà che hai letto un messaggio, a meno che tu non abbia disabilitato le conferme di lettura.</p>
<p>Se non desideri condividere il tuo profilo principale,
puoi anche creare un <a href="#multiple-accounts">profilo dedicato</a> per unirti a un canale.</p>
<h3 id="creare-un-canale">
Creare un canale <a href="#creare-un-canale" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tocca <strong>Nuova Chat</strong> e scegli <strong>Nuovo Canale</strong>.</p>
</li>
<li>
<p>Inserisci un <strong>nome</strong>, imposta facoltativamente un<strong>immagine</strong> e una <strong>descrizione</strong>, e fai clic sul pulsante <strong>Crea</strong>.</p>
</li>
<li>
<p>Ora puoi inviare e gestire i messaggi come di consueto.</p>
</li>
<li>
<p>Dal profilo del canale, <strong>condividi il codice QR o il collegamento di invito con altri</strong>.</p>
</li>
</ul>
<p>Gli iscritti riceveranno i tuoi messaggi,
ma non potranno inviare messaggi nel tuo canale.
Al momento delliscrizione, riceveranno <strong>alcuni degli ultimi messaggi della cronologia del canale</strong>.</p>
<p>Accanto a ciascun messaggio puoi vedere il <strong>numero di visualizzazioni</strong>.
Tieni presente che questo conteggio si riferisce solo agli abbonati che hanno attivato le conferme di lettura,
quindi il numero reale di visualizzazioni potrebbe essere superiore.</p>
<h3 id="quanti-iscritti-può-avere-un-canale">
Quanti iscritti può avere un canale? <a href="#quanti-iscritti-può-avere-un-canale" class="anchor"></a>
</h3>
<p>I canali sono progettati per un pubblico molto più ampio rispetto ai <a href="#groups">gruppi</a>.</p>
<p>Il limite pratico dipende dal numero di <a href="#relays">ripetitori</a> utilizzati, quindi non esiste un singolo numero fisso valido ovunque.</p>
<p>Per canali molto grandi con diverse decine di migliaia di iscritti,
consigliamo di utilizzare un <a href="#multiple-accounts">profilo dedicato</a> per il canale
e di verificare se il ripetitore è adatto.</p>
<p>Ma non esitare troppo: Delta Chat è progettato per essere indipendente dal ripetitore,
quindi puoi cambiare il tuo ripetitore in qualsiasi momento con facilità -
i tuoi iscritti esistenti non se ne accorgeranno nemmeno.
In tal caso, dovrai solo aggiornare il collegamento di invito che condividi con i nuovi iscritti.</p>
<h2 id="calls">
Chiamate <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1412,23 +1617,20 @@ non possono essere identificati facilmente.</p>
</h3>
<p>Il <a href="#relays">ripetitore</a> utilizzato deve conoscere il tuo indirizzo IP,
e talvolta anche i dispositivi dei tuoi contatti se avete una <a href="#experiments">chiamata</a>
o utilizzate <a href="#webxdc">apps</a> insieme.</p>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>Gli indirizzi IP sono necessari per la connettività e lefficienza.
Non sono né persistenti né esposti.
Si noti che lindirizzo IP
non è come un indirizzo dettagliato che si fornisce a un servizio di consegna,
ma molto più generico, che spesso definisce solo la regione o il paese.</p>
<p>IP Addresses are needed for connectivity and efficiency.
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>Poiché questo è il modo in cui Internet e altri servizi di messaggistica funzionano di default,
non offriamo opzioni né poniamo domande in anticipo.</p>
<p>Se ritieni che il tuo indirizzo IP rappresenti un rischio per la sicurezza o la privacy,
ti consigliamo di utilizzare una VPN, in combinazione con la modalità di blocco del sistema.
Esplorare le opzioni in tutte le app del tuo sistema lascerà delle lacune.
Ad esempio, cliccare su un link espone gli indirizzi IP a sconosciuti e rappresenta il rischio di gran lunga maggiore.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+216 -13
View File
@@ -29,6 +29,21 @@
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
@@ -622,6 +637,197 @@ but more than 150 is not recommended.</p>
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1419,23 +1625,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+276 -176
View File
@@ -2,31 +2,46 @@
<html lang="pl"><head><meta charset="UTF-8" /><meta name="viewport" content="initial-scale=1.0" /><link rel="stylesheet" href="../help.css" /></head><body><ul id="top">
<li><a href="#czym-jest-delta-chat">Czym jest Delta Chat?</a>
<ul>
<li><a href="#howtoe2ee">How can I find people to chat with?</a></li>
<li><a href="#why-is-a-chat-marked-as-request">Why is a chat marked as “Request”?</a></li>
<li><a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other">How can I put two of my friends in contact with each other?</a></li>
<li><a href="#howtoe2ee">Jak znaleźć osoby do czatu?</a></li>
<li><a href="#dlaczego-czat-jest-oznaczony-jako-prośba">Dlaczego czat jest oznaczony jako „Prośba”?</a></li>
<li><a href="#jak-mogę-skontaktować-ze-sobą-dwóch-znajomych">Jak mogę skontaktować ze sobą dwóch znajomych?</a></li>
<li><a href="#multiple-accounts">Czym są profile? Jak mogę przełączać się między nimi?</a></li>
<li><a href="#kto-widzi-moje-zdjęcie-profilowe">Kto widzi moje zdjęcie profilowe?</a></li>
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
<li><a href="#signature">Czy w Delta Chat mogę ustawić biografię/status?</a></li>
<li><a href="#co-oznacza-przypinanie-wyciszanie-i-archiwizowanie">Co oznacza przypinanie, wyciszanie i archiwizowanie?</a></li>
<li><a href="#save">Jak działają „Zapisane wiadomości”?</a></li>
<li><a href="#co-oznacza-zielona-kropka">Co oznacza zielona kropka?</a></li>
<li><a href="#co-oznaczają-znaczniki-wyświetlane-obok-wiadomości-wychodzących">Co oznaczają znaczniki wyświetlane obok wiadomości wychodzących?</a></li>
<li><a href="#edit">Poprawianie literówek i usuwanie wiadomości po wysłaniu</a></li>
<li><a href="#mediaquality">How is media quality handled?</a></li>
<li><a href="#mediaquality">Jak obsługiwana jest jakość multimediów?</a></li>
<li><a href="#ephemeralmsgs">Jak działają znikające wiadomości?</a></li>
<li><a href="#delold">Co się stanie, jeśli włączę opcję „Usuń wiadomości z urządzenia”?</a></li>
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
<li><a href="#remove-account">Jak mogę usunąć swój profil czatu?</a></li>
</ul>
</li>
<li><a href="#groups">Groups</a>
<li><a href="#groups">Grupy</a>
<ul>
<li><a href="#tworzenie-grupy">Tworzenie grupy</a></li>
<li><a href="#addmembers">Add and remove members</a></li>
<li><a href="#addmembers">Dodawanie i usuwanie członków</a></li>
<li><a href="#usunąłem-się-przez-przypadek">Usunąłem się przez przypadek.</a></li>
<li><a href="#nie-chcę-już-otrzymywać-wiadomości-od-grupy">Nie chcę już otrzymywać wiadomości od grupy.</a></li>
<li><a href="#cloning-a-group">Cloning a group</a></li>
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
<li><a href="#klonowanie-grupy">Klonowanie grupy</a></li>
<li><a href="#ilu-członków-może-należeć-do-jednej-grupy">Ilu członków może należeć do jednej grupy?</a></li>
</ul>
</li>
<li><a href="#channels">Kanały</a>
<ul>
<li><a href="#subskrybowanie-kanału">Subskrybowanie kanału</a></li>
<li><a href="#tworzenie-kanału">Tworzenie kanału</a></li>
<li><a href="#ilu-subskrybentów-może-mieć-kanał">Ilu subskrybentów może mieć kanał?</a></li>
</ul>
</li>
<li><a href="#calls">Połączenia</a>
<ul>
<li><a href="#nawiązywanie-połączenia">Nawiązywanie połączenia</a></li>
<li><a href="#odbieranie-lub-odrzucanie-połączenia">Odbieranie lub odrzucanie połączenia</a></li>
<li><a href="#w-trakcie-połączenia">W trakcie połączenia</a></li>
<li><a href="#nieodebrane-połączenia-i-powiadomienia">Nieodebrane połączenia i powiadomienia</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
@@ -107,87 +122,67 @@
<p>Delta Chat to niezawodna, zdecentralizowana i bezpieczna aplikacja do błyskawicznego przesyłania wiadomości, dostępna na platformy mobilne i stacjonarne.</p>
<p>Natychmiastowe tworzenie <strong>prywatnych profili czatu</strong> z bezpiecznymi i interoperacyjnymi <a href="https://chatmail.at/relays">przekaźnikami chatmail</a>, które oferują natychmiastowe dostarczanie wiadomości oraz powiadomienia push dla urządzeń z systemem iOS i Android.</p>
<ul>
<li>
<p>Instant creation of <strong>private chat profiles</strong>
with secure and interoperable <a href="https://chatmail.at/relays">chatmail relays</a>
that offer instant message delivery, and Push Notifications for iOS and Android devices.</p>
<p>Wszechstronna obsługa <a href="#multiple-accounts">wielu profili</a> i <a href="#multiclient">wielu urządzeń</a> na wszystkich platformach i między różnymi <a href="https://chatmail.at/clients">aplikacjami chatmail</a>.</p>
</li>
<li>
<p>Pervasive <a href="#multiple-accounts">multi-profile</a> and
<a href="#multiclient">multi-device</a> support on all platforms
and between different <a href="https://chatmail.at/clients">chatmail apps</a>.</p>
<p>Interaktywne <a href="#webxdc">aplikacje do czatu</a> w grach i do współpracy</p>
</li>
<li>
<p>Interactive <a href="#webxdc">in-chat apps</a> for gaming and collaboration</p>
<p><a href="#security-audits">Audytowne szyfrowanie end-to-end</a> zabezpieczające przed atakami sieciowymi i serwerowymi.</p>
</li>
<li>
<p><a href="#security-audits">Audited end-to-end encryption</a>
safe against network and server attacks.</p>
</li>
<li>
<p>Free and Open Source software, both app and server side,
built on <a href="https://github.com/chatmail/core/blob/main/standards.md#standards-used-in-delta-chat">Internet Standards</a>.</p>
<p>Bezpłatne i otwartoźródłowe oprogramowanie zarówno po stronie aplikacji, jak i serwera, stworzone w oparciu o <a href="https://github.com/chatmail/core/blob/main/standards.md#standards-used-in-delta-chat">standardy internetowe</a>.</p>
</li>
</ul>
<h3 id="howtoe2ee">
How can I find people to chat with? <a href="#howtoe2ee" class="anchor"></a>
Jak znaleźć osoby do czatu? <a href="#howtoe2ee" class="anchor"></a>
</h3>
<p>First, note that Delta Chat is a private messenger.
There is no public discovery, <em>you</em> decide about your contacts.</p>
<p>Najpierw pamiętaj, że Delta Chat to prywatny komunikator. Nie ma możliwości publicznego wyszukiwania, sam decydujesz o swoich kontaktach.</p>
<ul>
<li>
<p>If you are <strong>face to face</strong> with your friend or family,
tap the <strong>QR Code</strong> icon <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" />
on the main screen.<br />
Ask your chat partner to <strong>scan</strong> the QR image
with their Delta Chat app.</p>
<p>Jeśli jesteś twarzą w twarz ze znajomym lub rodziną, dotknij ikony <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>kodu QR</strong> na ekranie głównym.
Poproś partnera czatu o <strong>zeskanowanie</strong> obrazu QR za pomocą aplikacji Delta Chat.</p>
</li>
<li>
<p>For a <strong>remote</strong> contact setup,
from the same screen,
click “Copy” or “Share” and send the <strong>invite link</strong>
through another private chat.</p>
<p>Aby skonfigurować kontakt <strong>zdalny</strong>, na tym samym ekranie naciśnij „Kopiuj” lub „Udostępnij” i wyślij <strong>link zaproszenia</strong> za pośrednictwem innego prywatnego czatu.</p>
</li>
</ul>
<p>Now wait while connection gets established.</p>
<p>Poczekaj, aż połączenie zostanie nawiązane.</p>
<ul>
<li>
<p>If both sides are online, they will soon see a chat
and can start messaging securely.</p>
<p>Jeśli obie strony są online, wkrótce zobaczą czat i będą mogły bezpiecznie wysyłać wiadomości.</p>
</li>
<li>
<p>If one side is offline or in bad network,
the ability to chat is delayed until connectivity is restored.</p>
<p>Jeśli jedna ze stron jest offline lub ma słaby zasięg, możliwość czatowania zostanie wstrzymana do czasu przywrócenia połączenia.</p>
</li>
</ul>
<p>Congratulations!
You now will automatically use <a href="#e2ee">end-to-end encryption</a> with this contact.
If you add each other to <a href="#groups">groups</a>, end-to-end encryption will be established among all members.</p>
<p>Gratulacje! Teraz będziesz automatycznie korzystać z <a href="#e2ee">szyfrowania typu end-to-end</a> dla tego kontaktu. Jeśli dodacie się nawzajem do <a href="#groups">grup</a>, szyfrowanie typu end-to-end zostanie nawiązane między wszystkimi członkami.</p>
<h3 id="why-is-a-chat-marked-as-request">
<h3 id="dlaczego-czat-jest-oznaczony-jako-prośba">
Why is a chat marked as “Request”? <a href="#why-is-a-chat-marked-as-request" class="anchor"></a>
Dlaczego czat jest oznaczony jako „Prośba”? <a href="#dlaczego-czat-jest-oznaczony-jako-prośba" class="anchor"></a>
</h3>
<p>As being a private messenger,
only friends and family you <a href="#howtoe2ee">share your QR code or invite link with</a> can write to you.</p>
<p>Ponieważ jest to prywatny komunikator, tylko znajomi i rodzina, którym <a href="#howtoe2ee">udostępnisz swój kod QR lub link zaproszenia</a>, mogą do ciebie pisać.</p>
<p>Your friends may share your contact with other friends,
this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Request</b></p>
<p>Twoi znajomi mogą udostępniać twoje dane kontaktowe innym znajomym, co jest oznaczone jako <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Prośba</b></p>
<ul>
<li>
@@ -201,19 +196,17 @@ this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-s
</li>
</ul>
<h3 id="how-can-i-put-two-of-my-friends-in-contact-with-each-other">
<h3 id="jak-mogę-skontaktować-ze-sobą-dwóch-znajomych">
How can I put two of my friends in contact with each other? <a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other" class="anchor"></a>
Jak mogę skontaktować ze sobą dwóch znajomych? <a href="#jak-mogę-skontaktować-ze-sobą-dwóch-znajomych" class="anchor"></a>
</h3>
<p>Attach the first contact to the chat of the second using <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attachment ButtonContact</strong>.
You can also add a little introduction message.</p>
<p>Dołącz pierwszy kontakt do czatu drugiego, używając przycisku <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>DołączaniaKontakt</strong>. Możesz również dodać krótką wiadomość powitalną.</p>
<p>The second contact will receive a <strong>card</strong> then
and can tap it to start chatting with the first contact.</p>
<p>Drugi kontakt otrzyma wtedy <strong>kartkę</strong> i może ją nacisnąć, aby rozpocząć czat z pierwszym kontaktem.</p>
<h3 id="multiple-accounts">
@@ -223,15 +216,13 @@ and can tap it to start chatting with the first contact.</p>
</h3>
<p>A profile is <strong>a name, a picture</strong> and some additional information for encrypting messages.
A profile lives on your device(s) only
and uses the server only to relay messages.</p>
<p>Profil składa się z <strong>nazwy, zdjęcia</strong> i dodatkowych informacji służących do szyfrowania wiadomości. Profil jest dostępny tylko na twoim urządzeniu (urządzeniach) i korzysta z serwera wyłącznie do przekazywania wiadomości.</p>
<p>Podczas pierwszej instalacji Delta Chat tworzony jest pierwszy profil.</p>
<p>Później możesz dotknąć swojego zdjęcia profilowego w lewym górnym rogu, aby <strong>Dodać profile</strong> lub <strong>Przełączyć profile</strong>.</p>
<p>You may want to use separate profiles for political, family or work related activities.</p>
<p>Możesz używać osobnych profili dla aktywności politycznych, rodzinnych lub zawodowych.</p>
<p>Możesz także dowiedzieć się, <a href="#multiclient">jak używać tego samego profilu na wielu urządzeniach</a>.</p>
@@ -243,22 +234,19 @@ and uses the server only to relay messages.</p>
</h3>
<p>Możesz dodać zdjęcie profilowe w swoich ustawieniach. Jeśli napiszesz do swoich kontaktów lub dodasz je za pomocą kodu QR, automatycznie zobaczą je jako Twoje zdjęcie profilowe.</p>
<p>Możesz dodać zdjęcie profilowe w swoich ustawieniach. Jeśli napiszesz do swoich kontaktów lub dodasz je za pomocą kodu QR, automatycznie zobaczą je jako twoje zdjęcie profilowe.</p>
<p>Ze względów prywatności nikt nie widzi Twojego zdjęcia profilowego, dopóki nie napiszesz do niego wiadomości.</p>
<p>Ze względów prywatności nikt nie widzi twojego zdjęcia profilowego, dopóki nie napiszesz do niego wiadomości.</p>
<h3 id="signature">
Can I set a Bio/Status with Delta Chat? <a href="#signature" class="anchor"></a>
Czy w Delta Chat mogę ustawić biografię/status? <a href="#signature" class="anchor"></a>
</h3>
<p>Yes,
you can do so under <strong>Settings → Profile → Bio</strong>.
Once you sent a message to a contact,
they will see it when they view your contact details.</p>
<p>Tak, możesz to zrobić w <strong>Ustawieniach → Profil → Biografia</strong>. Po wysłaniu wiadomości do kontaktu zostanie ona wyświetlona, gdy będzie on przeglądał twoje dane kontaktowe.</p>
<h3 id="co-oznacza-przypinanie-wyciszanie-i-archiwizowanie">
@@ -278,7 +266,7 @@ they will see it when they view your contact details.</p>
<p><strong>Wycisz czaty</strong>, jeśli nie chcesz otrzymywać z nich powiadomień. Wyciszone czaty pozostają na swoim miejscu i możesz też przypiąć wyciszony czat.</p>
</li>
<li>
<p><strong>Archiwizuj czaty</strong>, jeśli nie chcesz ich już widzieć na liście czatów. Zarchiwizowane czaty pozostają dostępne nad listą czatów lub poprzez wyszukiwanie.</p>
<p><strong>Archiwizuj czaty</strong>, jeśli nie chcesz ich już widzieć na liście czatów. Pozostają dostępne nad listą czatów lub poprzez wyszukiwanie i są oznaczone jako <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Zarchiwizowane</b></p>
</li>
<li>
<p>Gdy zarchiwizowany czat otrzyma nową wiadomość, o ile nie zostanie wyciszony, <strong>wyskoczy z archiwum</strong> i wróci na twoją listę czatów.
@@ -309,7 +297,7 @@ they will see it when they view your contact details.</p>
<p>Później otwórz czat „Zapisane wiadomości” — zobaczysz tam zapisane wiadomości. Naciskając <img style="vertical-align:middle; width:1.2em; margin:1px" src="../go-to-original.png" alt="ikona strzałki w prawo" />, możesz wrócić do oryginalnej wiadomości w oryginalnym czacie</p>
</li>
<li>
<p>Na koniec możesz również użyć „Zapisz wiadomości”, aby robić <strong>osobiste notatki</strong> — otwórz czat, wpisz coś, dodaj zdjęcie lub wiadomość głosową itp.</p>
<p>Na koniec możesz również użyć „Zapisanych wiadomości”, aby robić <strong>osobiste notatki</strong> — otwórz czat, wpisz coś, dodaj zdjęcie lub wiadomość głosową itp.</p>
</li>
<li>
<p>Ponieważ „Zapisane wiadomości” są zsynchronizowane, mogą być bardzo przydatne do przesyłania danych między urządzeniami</p>
@@ -326,13 +314,9 @@ they will see it when they view your contact details.</p>
</h3>
<p>You can sometimes see a <strong>green dot</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />
next to the avatar of a contact.
It means they were <strong>recently seen by you</strong> in the last 10 minutes,
e.g. because they messaged you or sent a read receipt.</p>
<p>Czasami można zobaczyć <strong>zieloną kropkę</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" /> obok awatara kontaktu. Oznacza to, że był on <strong>niedawno widziany przez ciebie</strong> w ciągu ostatnich 10 minut, np. wysłał ci wiadomość lub potwierdzenie odczytu.</p>
<p>So this is not a real time online status
and others will as well not always see that you are “online”.</p>
<p>Nie jest to więc status online w czasie rzeczywistym i inni również nie zawsze zobaczą, że jesteś „online”.</p>
<h3 id="co-oznaczają-znaczniki-wyświetlane-obok-wiadomości-wychodzących">
@@ -344,19 +328,16 @@ and others will as well not always see that you are “online”.</p>
<ul>
<li>
<p><strong>One tick</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick1.png" alt="" />
means that the message was sent successfully to the <a href="#relays">relay</a>.</p>
<p><strong>Jeden znacznik</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick1.png" alt="" /> oznacza, że wiadomość została pomyślnie wysłana do <a href="#relays">przekaźnika</a>.</p>
</li>
<li>
<p><strong>Two ticks</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick2.png" alt="" />
indicate your contact has read the message.</p>
<p><strong>Dwa znaczniki</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick2.png" alt="" /> oznaczają, że twój kontakt przeczytał wiadomość.</p>
</li>
</ul>
<p>In <a href="#groups">groups</a> the second tick means that at least one member has reported back having read the message.</p>
<p>W <a href="#groups">grupach</a> drugi znacznik oznacza, że co najmniej jeden członek potwierdził przeczytanie wiadomości.</p>
<p>You will only get the second tick if both you and one of the recipients who read the message
has <strong>Settings → Chats → Read Receipts</strong> enabled.</p>
<p>Drugi znacznik pojawi się tylko wtedy, gdy ty i jeden z odbiorców, którzy przeczytali wiadomość, macie włączoną opcję <strong>Ustawienia → Czaty → Potwierdzenie odczytu</strong>.</p>
<h3 id="edit">
@@ -382,26 +363,22 @@ has <strong>Settings → Chats → Read Receipts</strong> enabled.</p>
<h3 id="mediaquality">
How is media quality handled? <a href="#mediaquality" class="anchor"></a>
Jak obsługiwana jest jakość multimediów? <a href="#mediaquality" class="anchor"></a>
</h3>
<p>Images, videos, files, voice messages etc. can be sent using the <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attach-</strong>
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons.</p>
<p>Obrazy, filmy, pliki, wiadomości głosowe itp. można wysyłać za pomocą przycisków: <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Załącz</strong> lub <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /><strong>Wiadomość głosowa</strong>.</p>
<ul>
<li>
<p>By default, compression ensures <strong>fast, efficient delivery</strong> that respects everyones data limits and storage.
This is ideal for everyday communication.</p>
<p>Domyślnie kompresja zapewnia <strong>szybką i wydajną dostawę</strong>, respektując limity danych i pamięci wszystkich użytkowników. Jest to idealne rozwiązanie do codziennej komunikacji.</p>
</li>
<li>
<p>In regions with worse connectivity,
you can choose higher compression at <strong>Settings → Chats → Outgoing Media Quality</strong>.</p>
<p>W regionach o słabszej łączności można wybrać wyższą kompresję w <strong>Ustawieniach → Czaty → Jakość mediów wychodzących</strong>.</p>
</li>
<li>
<p>If you specifically need to send media in its <strong>original quality</strong>, use <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attach → File</strong> in the chat.
Please use this method sparingly, as sending original files will significantly increase data usage for you and all recipients in the chat.</p>
<p>Jeśli chcesz wysłać multimedia w <strong>oryginalnej jakości</strong>, użyj w czacie opcji <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Załącz → Plik</strong>. Używaj tej metody oszczędnie, ponieważ wysyłanie oryginalnych plików znacznie zwiększy zużycie danych przez ciebie i wszystkich odbiorców na czacie.</p>
</li>
</ul>
@@ -415,21 +392,11 @@ Please use this method sparingly, as sending original files will significantly i
<p>Możesz włączyć „znikające wiadomości” w ustawieniach czatu, w prawym górnym rogu okna czatu, wybierając przedział czasu od 5 minut do 1 roku.</p>
<p>Until the setting is turned off again,
each chat members Delta Chat app takes care
of deleting the messages
after the selected time span.
The time span begins
when the receiver first sees the message in Delta Chat.
The messages are deleted both,
on the servers,
and in the apps itself.</p>
<p>Dopóki ustawienie nie zostanie ponownie wyłączone, aplikacja Delta Chat u każdego członka czatu zajmie się usuwaniem wiadomości po wybranym okresie. Przedział czasu rozpoczyna się w momencie, gdy odbiorca po raz pierwszy zobaczy wiadomość w Delta Chat. Wiadomości są usuwane zarówno na serwerze, jak i w samej aplikacji.</p>
<p>Pamiętaj, że na znikających wiadomościach możesz polegać tylko wtedy, gdy ufasz swoim partnerom czatu; złośliwi partnerzy czatu mogą robić zdjęcia lub w inny sposób zapisywać, kopiować lub przesyłać dalej wiadomości przed usunięciem.</p>
<p>Apart from that,
if one chat partner uninstalls Delta Chat,
the (anyway encrypted) messages may take longer to get deleted from their server.</p>
<p>Poza tym, jeśli jeden z uczestników czatu odinstaluje aplikację Delta Chat, usunięcie (i tak zaszyfrowanych) wiadomości z jego serwera może potrwać dłużej.</p>
<h3 id="delold">
@@ -439,46 +406,35 @@ the (anyway encrypted) messages may take longer to get deleted from their server
</h3>
<p>Jeśli chcesz zaoszczędzić miejsce na urządzeniu, możesz wybrać opcję automatycznego usuwania starych wiadomości.</p>
<p>Jeśli chcesz zaoszczędzić miejsce na swoim urządzeniu, możesz wybrać opcję automatycznego usuwania starych wiadomości.</p>
<p>Aby ją włączyć, przejdź do „Usuń wiadomości z urządzenia” w ustawieniach w sekcji „Czaty i media”. Możesz ustawić przedział czasowy pomiędzy „po 1 godzinie” a „po 1 roku”; w ten sposób <em>wszystkie</em> wiadomości zostaną usunięte z urządzenia, gdy tylko staną się starsze.</p>
<p>Aby ją włączyć, przejdź do <strong>Ustawienia → Czaty → Usuń wiadomości z urządzenia</strong> . Możesz ustawić przedział czasowy pomiędzy „po 1 godzinie” a „po 1 roku”; w ten sposób <em>wszystkie</em> wiadomości zostaną usunięte z urządzenia, gdy tylko staną się starsze.</p>
<h3 id="remove-account">
How can I delete my chat profile? <a href="#remove-account" class="anchor"></a>
Jak mogę usunąć swój profil czatu? <a href="#remove-account" class="anchor"></a>
</h3>
<p>If you are using more than one chat profile,
you can remove single ones in the top profile switcher menu (on Android and iOS),
or in the sidebar with a right click (in the Desktop app).
Chat profiles are only removed on the device where deletion was triggered.
Chat profiles on other devices will continue to fully function.</p>
<p>Jeśli używasz więcej niż jednego profilu czatu, możesz usunąć pojedyncze profile w górnym menu przełączania profili (na Androidzie i iOS) lub w pasku bocznym, klikając prawym przyciskiem myszy (w aplikacji na komputery). Profile czatu są usuwane tylko na urządzeniu, na którym nastąpiło usunięcie. Profile czatu na innych urządzeniach będą nadal w pełni działać.</p>
<p>If you use a single default chat profile you can simply uninstall the app.
This will still automatically trigger deletion of all associated address data on the chatmail server.
For more info, please refer to <a href="https://nine.testrun.org/info.html#account-deletion">nine.testrun.org address-deletion</a>
or the respective page from your chosen <a href="https://chatmail.at/relays">3rd party chatmail server</a>.</p>
<p>Jeśli używasz jednego domyślnego profilu czatu, możesz po prostu odinstalować aplikację. Spowoduje to automatyczne usunięcie wszystkich powiązanych danych adresowych na serwerze czatu. Aby uzyskać więcej informacji, zapoznaj się z informacjami o <a href="https://nine.testrun.org/info.html#account-deletion">usuwaniu adresów na stronie nine.testrun.org</a> lub odpowiednią stroną wybranego <a href="https://chatmail.at/relays">serwera czatu innej firmy</a>.</p>
<h2 id="groups">
Groups <a href="#groups" class="anchor"></a>
Grupy <a href="#groups" class="anchor"></a>
</h2>
<p>Groups let several people chat together privately with <strong>equal rights</strong>.</p>
<p>Grupy pozwalają kilku osobom na prywatną rozmowę na <strong>równych prawach</strong>.</p>
<p>Anyone can
change the group name or avatar,
<a href="#addmembers">add or remove members</a>,
set <a href="#ephemeralmsgs">disappearing messages</a>,
and <a href="#edit">delete their own messages</a> from all 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">
@@ -493,43 +449,37 @@ and <a href="#edit">delete their own messages</a> from all members devices.</
<p>Wybierz <strong>Nowy czat</strong>, a następnie <strong>Nowa grupa</strong> z menu w prawym górnym rogu lub naciśnij odpowiedni przycisk na Androidzie / iOS.</p>
</li>
<li>
<p>Na następnym ekranie wybierz <strong>członków grupy</strong> i zdefiniuj <strong>nazwę grupy</strong>. Możesz też wybrać awatar <strong>grupy</strong>.</p>
<p>Na następnym ekranie wybierz <strong>członków grupy</strong> i zdefiniuj <strong>nazwę grupy</strong>. Możesz też wybrać <strong>awatar grupy</strong>.</p>
</li>
<li>
<p>Zaraz po napisaniu pierwszej wiadomości w grupie wszyscy członkowie zostaną poinformowani o nowej grupie i mogą odpowiedzieć w grupie (jeżeli nie napiszesz wiadomości w grupie, grupa jest niewidoczna dla członków).</p>
<p>Gdy tylko napiszesz <strong>pierwszą wiadomość</strong> w grupie, wszyscy członkowie zostaną poinformowani o nowej grupie i będą mogli odpowiadać w grupie (dopóki nie napiszesz wiadomości w grupie, grupa będzie niewidoczna dla członków).</p>
</li>
</ul>
<h3 id="addmembers">
Add and remove members <a href="#addmembers" class="anchor"></a>
Dodawanie i usuwanie członków <a href="#addmembers" class="anchor"></a>
</h3>
<p>All group members have the <strong>same rights</strong>.
For this reason, everyone can delete any member or add new ones.</p>
<p>Wszyscy członkowie grupy mają <strong>takie same uprawnienia</strong>. Z tego powodu każdy może usunąć dowolnego członka lub dodać nowych.</p>
<ul>
<li>
<p>To <strong>add or delete members</strong>, tap the group name in the chat and select the member to add or remove.</p>
<p>Aby <strong>dodać lub usunąć członków</strong>, dotknij nazwę grupy na czacie i wybierz członka, którego chcesz dodać lub usunąć.</p>
</li>
<li>
<p>If the member is not yet in your contact list, but <strong>face to face</strong> with you,
from the same screen, show a <strong>QR code</strong>.<br />
Ask your chat partner to <strong>scan</strong> the QR image with their Delta Chat app by tapping
<img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> on the main screen.</p>
<p>Jeśli członek nie znajduje się jeszcze na twojej liście kontaktów, ale rozmawiasz z nim <strong>twarzą w twarz</strong>, na tym samym ekranie pokaż mu <strong>kod QR</strong>.
Poproś partnera czatu o <strong>zeskanowanie</strong> obrazu QR za pomocą aplikacji Delta Chat, dotykając <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> na ekranie głównym.</p>
</li>
<li>
<p>For a <strong>remote</strong> member addition,
click “Copy” or “Share” and send the <strong>invite link</strong>
through another private chat to the new member.</p>
<p>Aby dodać członka <strong>zdalnie</strong>, naciśnij „Kopiuj” lub „Udostępnij” i wyślij <strong>link zaproszenia</strong> nowemu członkowi za pośrednictwem innego prywatnego czatu.</p>
</li>
</ul>
<p>QR code and invite link can be used to add several members.
However, since groups are <a href="#groups">meant for trusted people</a>, avoid sharing them publicly.</p>
<p>Kod QR i link zaproszenia można wykorzystać do dodania kilku członków. Ponieważ jednak grupy są <a href="#groups">przeznaczone dla zaufanych osób</a>, unikaj udostępniania ich publicznie.</p>
<h3 id="usunąłem-się-przez-przypadek">
@@ -539,8 +489,7 @@ However, since groups are <a href="#groups">meant for trusted people</a>, avoid
</h3>
<p>Ponieważ nie jesteś członkiem grupy, nie możesz dodać siebie ponownie.
Jednak nie ma problemu, po prostu poproś dowolnego członka grupy na normalnym czacie, aby dodał cię ponownie.</p>
<p>Ponieważ nie jesteś członkiem grupy, nie możesz dodać siebie ponownie. Jednak nie ma problemu, po prostu poproś dowolnego członka grupy na normalnym czacie, aby dodał cię ponownie.</p>
<h3 id="nie-chcę-już-otrzymywać-wiadomości-od-grupy">
@@ -555,47 +504,203 @@ However, since groups are <a href="#groups">meant for trusted people</a>, avoid
Jeśli później będziesz chciał ponownie dołączyć do grupy, poproś innego członka grupy, aby dodał cię do grupy.</li>
</ul>
<p>Alternatywnie możesz też „Wyłączyć powiadomienia” dla grupy dzięki temu otrzymasz wszystkie wiadomości i
nadal będziesz mógł pisać, ale nie będziesz już powiadamiany o żadnych nowych wiadomościach.</p>
<p>Alternatywnie możesz też „Wyłączyć powiadomienia” dla grupy, dzięki temu otrzymasz wszystkie wiadomości i nadal będziesz mógł pisać, ale nie będziesz już powiadamiany o żadnych nowych wiadomościach.</p>
<h3 id="cloning-a-group">
<h3 id="klonowanie-grupy">
Cloning a group <a href="#cloning-a-group" class="anchor"></a>
Klonowanie grupy <a href="#klonowanie-grupy" class="anchor"></a>
</h3>
<p>You can duplicate a group to start a separate discussion
or to exclude members without them noticing.</p>
<p>Możesz zduplikować grupę, aby rozpocząć osobną dyskusję lub wykluczyć członków bez ich wiedzy.</p>
<ul>
<li>
<p>Open the group profile and tap <strong>Clone Chat</strong> (Android/iOS),
or right-click the group in the chat list (Desktop).</p>
<p>Otwórz profil grupy i dotknij opcji <strong>Klonuj czat</strong> (Android/iOS) lub kliknij prawym przyciskiem myszy grupę na liście czatów (komputer).</p>
</li>
<li>
<p>Set a new name, choose an avatar, and adjust the member list if needed.</p>
<p>Ustaw nową nazwę, wybierz awatar i w razie potrzeby dostosuj listę członków.</p>
</li>
</ul>
<p>The new group is <strong>fully independent</strong> from the original,
which continues to work as before.</p>
<p>Nowa grupa jest <strong>w pełni niezależna</strong> od oryginalnej, która nadal działa jak dotychczas.</p>
<h3 id="how-many-members-can-participate-in-a-single-group">
<h3 id="ilu-członków-może-należeć-do-jednej-grupy">
How many members can participate in a single group? <a href="#how-many-members-can-participate-in-a-single-group" class="anchor"></a>
Ilu członków może należeć do jednej grupy? <a href="#ilu-członków-może-należeć-do-jednej-grupy" class="anchor"></a>
</h3>
<p>There is no strict technical limit,
but more than 150 is not recommended.</p>
<p>Nie ma ścisłego limitu technicznego, ale nie zaleca się przekraczania 150 osób.</p>
<p>As groups get larger, they can become socially unstable and may need a hierarchy -
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">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">
Kanały <a href="#channels" class="anchor"></a>
</h2>
<p>Kanały to narzędzie typu jeden do wielu, służące do nadawania wiadomości.</p>
<h3 id="subskrybowanie-kanału">
Subskrybowanie kanału <a href="#subskrybowanie-kanału" class="anchor"></a>
</h3>
<ul>
<li>Zeskanuj <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>kod QR</strong> lub naciśnij link zaproszenia otrzymany od właściciela kanału.</li>
</ul>
<p>To wszystko! Otrzymasz kilka wiadomości z historii kanału, a od tego momentu wszystkie nowe wiadomości z kanału.</p>
<p><strong>Nie martw się</strong>, jeśli to nie nastąpi od razu. Gdy właściciel kanału będzie online, twoja prośba o dołączenie zostanie przetworzona.</p>
<p>Podobnie jak cała platforma Delta Chat, również kanały są prywatne i zdecentralizowane, dlatego nie ma możliwości publicznego ujawnienia.</p>
<p>Inni subskrybenci kanału nie zobaczą, że go subskrybujesz i nie będą mogli wysyłać ci wiadomości. Właściciel kanału może jednak napisać do ciebie. Zobaczy również, że przeczytałeś wiadomość, chyba że masz wyłączone potwierdzenia odczytu.</p>
<p>Jeśli nie chcesz udostępniać swojego głównego profilu, możesz również utworzyć <a href="#multiple-accounts">dedykowany profil</a> do dołączenia do kanału.</p>
<h3 id="tworzenie-kanału">
Tworzenie kanału <a href="#tworzenie-kanału" class="anchor"></a>
</h3>
<ul>
<li>
<p>Naciśnij <strong>Nowy cza</strong> i wybierz <strong>Nowy kanał</strong>.</p>
</li>
<li>
<p>Wprowadź <strong>nazwę</strong>, opcjonalnie ustaw <strong>obraz</strong> i <strong>opis</strong>, a następnie naciśnij przycisk <strong>Utwórz</strong>.</p>
</li>
<li>
<p>Możesz teraz wysyłać i zarządzać wiadomościami jak zwykle.</p>
</li>
<li>
<p>Z profilu kanału <strong>udostępnij kod QR lub link zaproszenia innym osobom</strong>.</p>
</li>
</ul>
<p>Subskrybenci będą otrzymywać twoje wiadomości, ale nie będą mogli wysyłać wiadomości na twoim kanale. Po zasubskrybowaniu otrzymają <strong>kilka najnowszych wiadomości z historii kanału</strong>.</p>
<p>Obok każdej wiadomości jest widoczna <strong>liczba wyświetleń</strong>. Pamiętaj, że uwzględnia ona tylko subskrybentów z włączonymi potwierdzeniami odczytu, więc rzeczywista liczba wyświetleń może być wyższa.</p>
<h3 id="ilu-subskrybentów-może-mieć-kanał">
Ilu subskrybentów może mieć kanał? <a href="#ilu-subskrybentów-może-mieć-kanał" class="anchor"></a>
</h3>
<p>Kanały są przeznaczone dla znacznie większej publiczności niż <a href="#groups">grupy</a>.</p>
<p>Praktyczny limit zależy od używanego <a href="#relays">przekaźnika</a>, więc nie ma jednej, stałej liczby, która obowiązywałaby wszędzie.</p>
<p>W przypadku naprawdę dużych kanałów z dziesiątkami tysięcy subskrybentów zalecamy użycie <a href="#multiple-accounts">dedykowanego profilu</a> dla kanału i sprawdzenie, czy przekaźnik jest odpowiedni.</p>
<p>Ale nie wahaj się zbytnio: Delta Chat został zaprojektowany tak, aby nie był zależny od przekaźnika, więc możesz go łatwo zmienić w dowolnym momencie twoi obecni subskrybenci nawet tego nie zauważą. W takim przypadku wystarczy zaktualizować link zaproszenia udostępniany nowym subskrybentom.</p>
<h2 id="calls">
Połączenia <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat umożliwia indywidualne <strong>połączenia audio i wideo</strong>.</p>
<p>Połączenia są obsługiwane na komputerach stacjonarnych, Ubuntu Touch, iOS oraz Androidzie 8 i nowszych.</p>
<h3 id="nawiązywanie-połączenia">
Nawiązywanie połączenia <a href="#nawiązywanie-połączenia" class="anchor"></a>
</h3>
<ul>
<li>
<p>W czacie indywidualnym dotknij <strong>ikony połączenia</strong> 📞.</p>
</li>
<li>
<p>Otworzy się małe menu, w którym możesz wybrać, czy chcesz nawiązać <strong>połączenie audio</strong>, czy <strong>wideo</strong>.</p>
</li>
</ul>
<h3 id="odbieranie-lub-odrzucanie-połączenia">
Odbieranie lub odrzucanie połączenia <a href="#odbieranie-lub-odrzucanie-połączenia" class="anchor"></a>
</h3>
<ul>
<li>
<p>Gdy ktoś do ciebie dzwoni, Delta Chat wyświetla <strong>ekran połączenia przychodzącego</strong> lub powiadomienie.</p>
</li>
<li>
<p>Dotknij <strong>Akceptuj</strong>, aby odebrać, lub <strong>Odrzuć</strong>, aby odrzucić połączenie.</p>
</li>
</ul>
<h3 id="w-trakcie-połączenia">
W trakcie połączenia <a href="#w-trakcie-połączenia" class="anchor"></a>
</h3>
<ul>
<li>
<p>Możesz <strong>wyciszyć</strong> mikrofon.</p>
</li>
<li>
<p>Możesz <strong>włączyć lub wyłączyć kamerę</strong>.</p>
</li>
<li>
<p>Na urządzeniach mobilnych możesz <strong>przełączać się między przednią i tylną kamerą</strong>.</p>
</li>
</ul>
<p>W zależności od urządzenia możesz również wybrać wyjście audio lub skorzystać z trybu obrazu w obrazie. Na komputerach stacjonarnych połączenie jest wyświetlane w dedykowanym oknie i możesz kontynuować korzystanie z głównego okna Delta Chat jak zwykle.</p>
<h3 id="nieodebrane-połączenia-i-powiadomienia">
Nieodebrane połączenia i powiadomienia <a href="#nieodebrane-połączenia-i-powiadomienia" class="anchor"></a>
</h3>
<ul>
<li>
<p>Jeśli nie odbierzesz, nie usłyszysz dzwonka lub nie będziesz mieć urządzenia pod ręką, połączenie zostanie oznaczone jako <strong>nieodebrane</strong>.</p>
</li>
<li>
<p><strong>Tylko zaakceptowane przez ciebie kontakty</strong> mogą wywołać dzwonek na urządzeniu. Prośby o kontakt będą wyświetlane normalnie i nie będą dzwonić.</p>
</li>
<li>
<p>W <strong>Ustawienia → Powiadomienia → Połączenia</strong> możesz całkowicie wyłączyć specjalny ekran dzwonka. Jeśli to zrobisz, nie będzie ci przeszkadzał żaden dzwonek, a połączenie nadal będzie można odebrać, dotykając w czacie dymku wiadomości o przychodzącym połączeniu.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -849,8 +954,7 @@ Welcome to the power of the interoperable chatmail relay network :)</p>
<p>Sprawdź dokładnie, czy oba urządzenia są w tym <strong>samym Wi-Fi lub tej samej sieci</strong></p>
</li>
<li>
<p>Na <strong>Windowsie</strong>, przejdź do Panel sterowania / Sieć i internet” i upewnij się, że <strong>Sieć prywatna</strong> jest wybrana jako Typ profilu sieci”
(po przeniesieniu możesz wrócić do pierwotnej wartości)</p>
<p>Na <strong>Windowsie</strong>, przejdź do Panel sterowania / Sieć i internet” i upewnij się, że <strong>Sieć prywatna</strong> jest wybrana jako Typ profilu sieci” (po przeniesieniu możesz wrócić do pierwotnej wartości)</p>
</li>
<li>
<p>W systemie <strong>iOS</strong> upewnij się, że jest przydzielony dostęp do opcji „Ustawienia » Aplikacje » Delta Chat » <strong>Sieć lokalna</strong></p>
@@ -894,15 +998,14 @@ Welcome to the power of the interoperable chatmail relay network :)</p>
<ul>
<li>
<p>Na starym urządzeniu przejdź do <strong>Ustawienia → Czaty i media → Eksport kopii zapasowej</strong>. Wprowadź swój PIN odblokowania ekranu, wzór lub hasło. Następnie możesz nacisnąć „Utwórz kopię”. Spowoduje to zapisanie pliku kopii zapasowej na urządzeniu. Teraz musisz jakoś przenieść go na inne urządzenie.</p>
<p>Na starym urządzeniu przejdź do <strong>Ustawienia → Czaty → Eksport kopii zapasowej</strong>. Wprowadź swój PIN odblokowania ekranu, wzór lub hasło. Następnie możesz nacisnąć „Utwórz kopię”. Spowoduje to zapisanie pliku kopii zapasowej na urządzeniu. Teraz musisz jakoś przenieść go na inne urządzenie.</p>
</li>
<li>
<p>Na nowym urządzeniu, na ekranie logowania, zamiast logować się na swoje konto e-mail, wybierz <strong>Przywróć z kopii zapasowej</strong>. Po zaimportowaniu Twoje rozmowy, klucze szyfrujące i multimedia powinny zostać skopiowane na nowe urządzenie.
Jeśli korzystasz z iOS i napotykasz trudności, może <a href="https://support.delta.chat/t/import-backup-to-ios/1628">ten poradnik</a> Ci pomoże.</p>
<p>Na nowym urządzeniu wybierz: <strong>Mam już profil → Przywróć z kopii zapasowej</strong>. Jeśli korzystasz z iOS i napotykasz trudności, może <a href="https://support.delta.chat/t/import-backup-to-ios/1628">ten poradnik</a> ci pomoże.</p>
</li>
</ul>
<p>Jesteś teraz zsynchronizowany i możesz używać obu urządzeń do wysyłania i odbierania wiadomości zaszyfrowanych end-to-end w komunikacji ze swoimi partnerami.</p>
<p>Jesteś teraz zsynchronizowany i w komunikacji ze swoimi partnerami możesz używać obu urządzeń do wysyłania i odbierania wiadomości zaszyfrowanych metodą end-to-end.</p>
<h3 id="czy-są-jakieś-plany-wprowadzenia-klienta-web-delta-chat">
@@ -1289,23 +1392,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+216 -13
View File
@@ -29,6 +29,21 @@
<li><a href="#quantos-membros-podem-participar-em-um-único-grupo">Quantos membros podem participar em um único grupo?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">Aplicativos embutidos</a>
<ul>
<li><a href="#onde-posso-obter-aplicativos-embutidos">Onde posso obter aplicativos embutidos?</a></li>
@@ -622,6 +637,197 @@ mas não é recomendável mais de 150.</p>
mas o Delta Chat é um mensageiro privado para conversar com <a href="#groups">direitos iguais</a>.
Consulte o <a href="https://pt.wikipedia.org/wiki/N%C3%BAmero_de_Dunbar">Número de Dunbar</a> para obter mais informações.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1420,23 +1626,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+218 -17
View File
@@ -29,6 +29,21 @@
<li><a href="#сколько-участников-может-быть-в-одной-группе">Сколько участников может быть в одной группе?</a></li>
</ul>
</li>
<li><a href="#channels">Каналы</a>
<ul>
<li><a href="#подписка-на-канал">Подписка на канал</a></li>
<li><a href="#создание-канала">Создание канала</a></li>
<li><a href="#какое-максимальное-количество-подписчиков-может-быть-у-канала">Какое максимальное количество подписчиков может быть у канала?</a></li>
</ul>
</li>
<li><a href="#calls">Звонки</a>
<ul>
<li><a href="#как-сделать-звонок">Как сделать звонок</a></li>
<li><a href="#принять-или-отклонить-вызов">Принять или отклонить вызов</a></li>
<li><a href="#во-время-звонка">Во время звонка</a></li>
<li><a href="#пропущенные-вызовы-и-уведомления">Пропущенные вызовы и уведомления</a></li>
</ul>
</li>
<li><a href="#webxdc">Встроенные приложения чата</a>
<ul>
<li><a href="#где-можно-найти-встроенные-приложения">Где можно найти встроенные приложения?</a></li>
@@ -622,6 +637,197 @@
в то время как Delta Chat - это приватный мессенджер для общения на <a href="#groups">равных правах</a>.
Смотрите <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">число Данбара</a> для более глубокого понимания.</p>
<h2 id="channels">
Каналы <a href="#channels" class="anchor"></a>
</h2>
<p>Каналы представляют собой инструмент типа “один-ко-многим” для трансляции сообщений.</p>
<h3 id="подписка-на-канал">
Подписка на канал <a href="#подписка-на-канал" class="anchor"></a>
</h3>
<ul>
<li>Отсканируйте <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR-код</strong>
или нажмите на ссылку-приглашение, которую вы получили от владельца канала.</li>
</ul>
<p>Всё готово!
Сначала вы получите несколько сообщений из истории канала,
а затем — все новые сообщения, поступающие в него.</p>
<p><strong>Не беспокойтесь,</strong> если это произойдет не сразу.
Как только владелец канала выйдет в сеть, ваш запрос на вступление будет обработан.</p>
<p>Как и весь Delta Chat, каналы являются приватными и децентрализованными,
поэтому возможность публичного поиска каналов отсутствует.</p>
<p>Другие подписчики канала не увидят факта вашей подписки и не смогут отправлять вам сообщения.
Однако, владелец канала сможет отправить вам сообщение.
Также он будет видеть, что вы прочитали сообщение, если только вы не отключите подтверждение о прочтении.</p>
<p>Если вы не хотите использовать свой основной профиль,
вы можете создать <a href="#multiple-accounts">специальный профиль</a> для подписки на канал.</p>
<h3 id="создание-канала">
Создание канала <a href="#создание-канала" class="anchor"></a>
</h3>
<ul>
<li>
<p>Нажмите <strong>Новый чат</strong> и выберите <strong>Новый канал</strong>.</p>
</li>
<li>
<p>Введите <strong>название</strong>, по желанию добавьте <strong>изображение</strong> и <strong>описание</strong>, а затем нажмите кнопку <strong>Создать</strong>.</p>
</li>
<li>
<p>Теперь вы можете отправлять сообщения и управлять ими в обычном режиме.</p>
</li>
<li>
<p>В профиле канала вы можете <strong>поделиться QR-кодом или ссылкой-приглашением с другими пользователями</strong>.</p>
</li>
</ul>
<p>Подписчики будут получать ваши сообщения,
но не смогут отправлять сообщения в вашем канале.
При подписке они получат <strong>несколько последних сообщений из истории канала</strong>.</p>
<p>Рядом с каждым сообщением вы можете увидеть <strong>количество просмотров</strong>.
Обратите внимание, что учитываются только те подписчики, у которых включены уведомления о прочтении,
поэтому реальное количество просмотров может быть больше.</p>
<h3 id="какое-максимальное-количество-подписчиков-может-быть-у-канала">
Какое максимальное количество подписчиков может быть у канала? <a href="#какое-максимальное-количество-подписчиков-может-быть-у-канала" class="anchor"></a>
</h3>
<p>Каналы предназначены для гораздо более широкой аудитории, чем <a href="#groups">группы</a>.</p>
<p>Практический предел зависит от используемого <a href="#relays">релея</a>,
поэтому не существует единого фиксированного значения, применимого во всех случаях.</p>
<p>Для крайне крупных каналов с десятками тысяч подписчиков,
мы рекомендуем использовать <a href="#multiple-accounts">специальный профиль</a> для управления каналом,
а также предварительно проверить пригодность релея.</p>
<p>Не стоит опасаться: архитектура Delta Chat позволяет использовать любой релей (relay-agnostic),
поэтому вы можете легко изменить его в любой момент -
ваши текущие подписчики этого даже не заметят.
В этом случае достаточно будет обновить ссылку-приглашение, которую вы передаёте новым пользователям.</p>
<h2 id="calls">
Звонки <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat поддерживает <strong>аудио-</strong> и <strong>видеозвонки</strong> в режиме “один-на-один”.</p>
<p>Звонки работают на ПК, Ubuntu Touch, iOS и Android версии 8 и новее.</p>
<h3 id="как-сделать-звонок">
Как сделать звонок <a href="#как-сделать-звонок" class="anchor"></a>
</h3>
<ul>
<li>
<p>В чате “один-на-один” нажмите на 📞 <strong>значок вызова</strong>.</p>
</li>
<li>
<p>Откроется небольшое меню
в котором вы сможете выбрать вид связи <strong>аудио-</strong> или <strong>видеозвонок</strong>.</p>
</li>
</ul>
<h3 id="принять-или-отклонить-вызов">
Принять или отклонить вызов <a href="#принять-или-отклонить-вызов" class="anchor"></a>
</h3>
<ul>
<li>
<p>При входящем звонке,
Delta Chat показывает <strong>экран входящего вызова</strong> или уведомление.</p>
</li>
<li>
<p>Нажмите <strong>Принять</strong> чтобы ответить
или <strong>Отклонить</strong> чтобы сбросить звонок.</p>
</li>
</ul>
<h3 id="во-время-звонка">
Во время звонка <a href="#во-время-звонка" class="anchor"></a>
</h3>
<ul>
<li>
<p>Вы можете <strong>отключить</strong> звук микрофона.</p>
</li>
<li>
<p>Вы можете <strong>включить или выключить камеру</strong>.</p>
</li>
<li>
<p>На мобильных устройствах можно <strong>переключаться между фронтальной и основной камерами</strong>.</p>
</li>
</ul>
<p>В зависимости от устройства вы можете выбрать источник аудиовыхода или использовать режим “картинка в картинке”.
В приложении для ПК звонок осуществляется в отдельном окне,
что позволяет продолжать работу в основном окне Delta Chat в обычном режиме.</p>
<h3 id="пропущенные-вызовы-и-уведомления">
Пропущенные вызовы и уведомления <a href="#пропущенные-вызовы-и-уведомления" class="anchor"></a>
</h3>
<ul>
<li>
<p>Если вы не ответите на звонок, не услышите сигнал или ваше устройство будет недоступно,
вызов отобразится как <strong>пропущенный</strong>.</p>
</li>
<li>
<p><strong>Только ваши подтвержденные контакты</strong> могут заставить ваше устройство звонить.
Запросы на добавление в контакты будут приходить как обычно, но вызова не будет.</p>
</li>
<li>
<p>В разделе <strong>Настройки → Уведомления → Звонки</strong>,
вы можете полностью отключить специальный экран входящего вызова.
Если вы это сделаете, никакие уведомления о звонках не будут вас беспокоить;
при этом вы всё равно сможете принять звонок, нажав на иконку сообщения о входящем звонке в соответствующем чате.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1418,25 +1624,20 @@ Delta Chat вместо этого использует реализацию Ope
</h3>
<p>Используемый <a href="#relays">релей</a> должен знать ваш IP-адрес,
а также иногда устройства ваших контактов, если вы проводите совместные <a href="#experiments">звонки</a>
или используете <a href="#webxdc">приложения</a>.</p>
<p>Используемым <a href="#relays">релеям</a> необходимо знать ваш IP-адрес,
а в некоторых случаях — данные устройств ваших контактов, если вы совершаете <a href="#calls">вызов</a>
или совместно используете <a href="#webxdc">приложения</a>.</p>
<p>IP-адреса необходимы для обеспечения соединения и эффективности.
Они не сохраняются и не передаются третьим лицам.
Обратите внимание, что IP-адрес</p>
<ul>
<li>это не подробный адрес, который вы указываете службе доставки,
а скорее приблизительный, обычно определяющий регион или страну.</li>
</ul>
<p>IP-адреса необходимы для обеспечения связи и эффективной работы.
Delta Chat не сохраняет их и не раскрывает третьим лицам.
Обратите внимание, что IP-адрес
— это не тот же адрес, который вы указываете службе доставки,
он, как правило, менее точен и зачастую позволяет определить лишь город или регион.</p>
<p>Поскольку именно так по умолчанию работает интернет и другие мессенджеры,
мы не предлагаем здесь никаких настроек и не задаём предварительных вопросов</p>
<p>Если вы считаете свой IP-адрес угрозой безопасности или конфиденциальности,
мы рекомендуем использовать VPN в сочетании с режимом блокировки системы.
Поиск настроек во всех приложениях на вашем устройстве оставит уязвимости.
Например, нажатие на ссылку раскрывает IP-адрес неизвестным лицам и представляет собой гораздо больший риск в данном случае.</p>
<p>Если вы считаете свой IP-адрес зоной риска,
мы рекомендуем использовать VPN для всей системы.
Настройка VPN для отдельных приложений оставляет уязвимости в общей защите устройства.
Например, нажатие на ссылку может раскрыть ваш IP-адрес неизвестным сторонам, что представляет собой гораздо больший риск.</p>
<h3 id="sealedsender">
+216 -13
View File
@@ -29,6 +29,21 @@
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
@@ -627,6 +642,197 @@ but more than 150 is not recommended.</p>
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1425,23 +1631,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+216 -13
View File
@@ -29,6 +29,21 @@
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
@@ -627,6 +642,197 @@ but more than 150 is not recommended.</p>
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1427,23 +1633,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+230 -40
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>
@@ -28,6 +28,21 @@
<li><a href="#how-many-members-can-participate-in-a-single-group">How many members can participate in a single group?</a></li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">In-chat apps</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
@@ -151,8 +166,7 @@
<ul>
<li>
<p>If both sides are online, they will soon see a chat
and can start messaging securely.</p>
<p>Якщо обидві сторони перебувають у мережі, незабаром з’явиться вікно чату, і вони зможуть безпечно обмінюватися повідомленнями.</p>
</li>
<li>
<p>If one side is offline or in bad network,
@@ -164,19 +178,17 @@ the ability to chat is delayed until connectivity is restored.</p>
Тепер Ви автоматично використовуватимете <a href="#e2ee">наскрізне шифрування</a> з цим контактом.
Якщо ви додасте один одного у <a href="#groups">групи</a>, наскрізне шифрування буде встановлено між усіма учасниками.</p>
<h3 id="why-is-a-chat-marked-as-request">
<h3 id="чому-чат-позначений-як-запит">
Why is a chat marked as “Request”? <a href="#why-is-a-chat-marked-as-request" class="anchor"></a>
Чому чат позначений як «Запит»? <a href="#чому-чат-позначений-як-запит" class="anchor"></a>
</h3>
<p>As being a private messenger,
only friends and family you <a href="#howtoe2ee">share your QR code or invite link with</a> can write to you.</p>
<p>Оскільки це приватний месенджер, лише друзі та родичі, яким ви <a href="#howtoe2ee">надіслали свій QR-код або посилання-запрошення</a>, можуть вам писати.</p>
<p>Your friends may share your contact with other friends,
this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Request</b></p>
<p>Ваші друзі можуть поділитися вашими контактними даними з іншими друзями, це відображається як <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Запит</b></p>
<ul>
<li>
@@ -190,19 +202,17 @@ this appears as <b style="border: 1px solid currentColor; padding: 0 3px; font-s
</li>
</ul>
<h3 id="how-can-i-put-two-of-my-friends-in-contact-with-each-other">
<h3 id="як-я-можу-познайомити-двох-своїх-друзів-один-з-одним">
How can I put two of my friends in contact with each other? <a href="#how-can-i-put-two-of-my-friends-in-contact-with-each-other" class="anchor"></a>
Як я можу познайомити двох своїх друзів один з одним? <a href="#як-я-можу-познайомити-двох-своїх-друзів-один-з-одним" class="anchor"></a>
</h3>
<p>Attach the first contact to the chat of the second using <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attachment Button → Contact</strong>.
You can also add a little introduction message.</p>
<p>Додайте перший контакт до чату другого, скориставшись функцією <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Кнопка «Додати» → Контакт</strong>. Ви також можете додати коротке повідомлення-представлення.</p>
<p>The second contact will receive a <strong>card</strong> then
and can tap it to start chatting with the first contact.</p>
<p>Тоді другий контакт отримає <strong>картку</strong> і зможе натиснути на неї, щоб почати чат із першим контактом.</p>
<h3 id="multiple-accounts">
@@ -212,9 +222,7 @@ and can tap it to start chatting with the first contact.</p>
</h3>
<p>A profile is <strong>a name, a picture</strong> and some additional information for encrypting messages.
A profile lives on your device(s) only
and uses the server only to relay messages.</p>
<p>Профіль — це <strong>ім’я, зображення</strong> та деяка додаткова інформація для шифрування повідомлень. Профіль зберігається виключно на вашому пристрої (пристроях) і використовує сервер лише для передачі повідомлень.</p>
<p>Під час першого встановлення Delta Chat створюється перший профіль.</p>
@@ -244,10 +252,7 @@ and uses the server only to relay messages.</p>
</h3>
<p>Yes,
you can do so under <strong>Settings → Profile → Bio</strong>.
Once you sent a message to a contact,
they will see it when they view your contact details.</p>
<p>Так, ви можете це зробити в розділі <strong>Налаштування → Профіль → Опис</strong>. Після того як ви надішлете повідомлення контакту, він побачить його, переглянувши ваші контактні дані.</p>
<h3 id="що-значить-закріплення-приглушення-архівування">
@@ -315,10 +320,7 @@ they will see it when they view your contact details.</p>
</h3>
<p>You can sometimes see a <strong>green dot</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />
next to the avatar of a contact.
It means they were <strong>recently seen by you</strong> in the last 10 minutes,
e.g. because they messaged you or sent a read receipt.</p>
<p>Іноді біля аватара контакту можна побачити <strong>зелену крапку</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />. Це означає, що ви <strong>нещодавно бачили його</strong> протягом останніх 10 хвилин, наприклад, тому що він надіслав вам повідомлення або підтвердження прочитання.</p>
<p>So this is not a real time online status
and others will as well not always see that you are “online”.</p>
@@ -582,6 +584,197 @@ but more than 150 is not recommended.</p>
where Delta Chat is a private messenger for chatting with <a href="#groups">equal rights</a>.
See <a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">Dunbars number</a> for more insights.</p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1268,23 +1461,20 @@ can not be identified easily.</p>
</h3>
<p>The used <a href="#relays">relay</a> needs to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#experiments">call</a>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP Addresses are needed for connectivity and efficiency.
They are neither persisted nor exposed.
Note that the IP Address
is not like a detailed address you give to a delivery service,
but much more coarse, often defining region or country only.</p>
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>As this is just how the internet and other messengers work by default,
we do not offer options here or ask upfront questions.</p>
<p>If you see your IP Address as a security or privacy risk,
we recommend to use a VPN, in combination with system lockdown mode.
Hunting down options in all apps on your system will leave gaps.
For example, tapping a link exposes IP Addresses to unknown parties and is the by far larger risk here.</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<h3 id="sealedsender">
+218 -14
View File
@@ -32,6 +32,21 @@
</li>
</ul>
</li>
<li><a href="#channels">Channels</a>
<ul>
<li><a href="#subscribe-to-a-channel">Subscribe to a channel</a></li>
<li><a href="#create-a-channel">Create a channel</a></li>
<li><a href="#how-many-subscribers-can-a-channel-have">How many subscribers can a channel have?</a></li>
</ul>
</li>
<li><a href="#calls">Calls</a>
<ul>
<li><a href="#place-a-call">Place a call</a></li>
<li><a href="#accept-or-reject-a-call">Accept or reject a call</a></li>
<li><a href="#during-a-call">During a call</a></li>
<li><a href="#missed-calls-and-notifications">Missed calls and notifications</a></li>
</ul>
</li>
<li><a href="#webxdc">Webxdc 应用</a>
<ul>
<li><a href="#我在哪里可以获得-webxdc-应用">我在哪里可以获得 Webxdc 应用?</a></li>
@@ -621,6 +636,197 @@ Please use this method sparingly, as sending original files will significantly i
其中Delta Chat 是与<a href="#groups">平等权利</a> 聊天的私人信使。
相关知识,请参阅<a href="https://en.wikipedia.org/wiki/Dunbar%27s_number">邓巴数</a></p>
<h2 id="channels">
Channels <a href="#channels" class="anchor"></a>
</h2>
<p>Channels are a one-to-many tool for broadcasting messages.</p>
<h3 id="subscribe-to-a-channel">
Subscribe to a channel <a href="#subscribe-to-a-channel" class="anchor"></a>
</h3>
<ul>
<li>Scan the <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> <strong>QR code</strong>
or tap the <strong>invite link</strong> you got from the channel owner.</li>
</ul>
<p>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><strong>Dont worry,</strong> if that does not happen immediately.
Once the channel owner comes online, your join request will be processed.</p>
<p>As all of Delta Chat, also Channels are private and decentralized,
there is no public discovery.</p>
<p>Other channel subscribers will not see that you subscribed and cannot message you.
The channel owner, however, can message you.
They will also see that you read a message unless you have read receipts disabled.</p>
<p>If you do not want to share your main profile,
you can also create a <a href="#multiple-accounts">dedicated profile</a> for joining a channel.</p>
<h3 id="create-a-channel">
Create a channel <a href="#create-a-channel" class="anchor"></a>
</h3>
<ul>
<li>
<p>Tap <strong>New Chat</strong> and choose <strong>New Channel</strong>.</p>
</li>
<li>
<p>Enter a <strong>name</strong>, optionally set an <strong>image</strong> and <strong>description</strong>, and hit the <strong>Create</strong> button.</p>
</li>
<li>
<p>You can now send and manage messages as usual.</p>
</li>
<li>
<p>From the channels profile, <strong>share the QR code or invite link with others</strong>.</p>
</li>
</ul>
<p>Subscribers will receive your messages,
but they cannot send messages in your channel.
When subscribing, they will receive <strong>a few of the latest messages of the channel history</strong>.</p>
<p>You can see the <strong>view count</strong> beside each message.
Note that this only counts subscribers who have read receipts enabled,
so the real view count may be larger.</p>
<h3 id="how-many-subscribers-can-a-channel-have">
How many subscribers can a channel have? <a href="#how-many-subscribers-can-a-channel-have" class="anchor"></a>
</h3>
<p>Channels are designed for much larger audiences than <a href="#groups">groups</a>.</p>
<p>The practical limit depends on the used <a href="#relays">relay</a>,
so there is no single fixed number that applies everywhere.</p>
<p>For really large channels with several tens of thousands of subscribers,
we recommend using a <a href="#multiple-accounts">dedicated profile</a> for the channel
and checking whether the relay is suitable.</p>
<p>But 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>
<h2 id="calls">
Calls <a href="#calls" class="anchor"></a>
</h2>
<p>Delta Chat supports one-to-one <strong>audio calls</strong> and <strong>video calls</strong>.</p>
<p>Calls are supported on Desktop, Ubuntu Touch, iOS and Android 8 and newer.</p>
<h3 id="place-a-call">
Place a call <a href="#place-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>In a one-to-one chat, tap the 📞 <strong>call icon</strong>.</p>
</li>
<li>
<p>This opens a small menu
where you can choose whether to place an <strong>Audio Call</strong> or a <strong>Video Call</strong>.</p>
</li>
</ul>
<h3 id="accept-or-reject-a-call">
Accept or reject a call <a href="#accept-or-reject-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>When someone calls you,
Delta Chat shows an <strong>incoming call screen</strong> or notification.</p>
</li>
<li>
<p>Tap <strong>Accept</strong> to answer
or <strong>Decline</strong> to reject the call.</p>
</li>
</ul>
<h3 id="during-a-call">
During a call <a href="#during-a-call" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can <strong>mute</strong> your microphone.</p>
</li>
<li>
<p>You can <strong>enable or disable your camera</strong>.</p>
</li>
<li>
<p>On mobile, you can <strong>switch between front and back cameras</strong>.</p>
</li>
</ul>
<p>Depending on the device, you can also select the audio output or use picture-in-picture.
On desktop, the call is using a dedicated window
and you can continue using the main Delta Chat window as usual.</p>
<h3 id="missed-calls-and-notifications">
Missed calls and notifications <a href="#missed-calls-and-notifications" class="anchor"></a>
</h3>
<ul>
<li>
<p>If you do not answer, do not hear the ringing, or do not have your device at hand,
the call appears as a <strong>missed call</strong>.</p>
</li>
<li>
<p><strong>Only your accepted contacts</strong> can make your device ring.
Contact requests will appear as usual and will not ring.</p>
</li>
<li>
<p>At <strong>Settings → Notifications → Calls</strong>,
you can disable the special call ringing screen completely.
If you do so, you will not be disturbed by any ringing notification,
you can still pick up the call by tapping the incoming call message bubble in its chat.</p>
</li>
</ul>
<h2 id="webxdc">
@@ -1414,22 +1620,20 @@ Delta Chat 应用程序不会在服务器上存储任何有关联系人或群组
</h4>
<p>使用的 <a href="#relays">中继服务器</a> 需要知道您的 IP 地址、
有时还需要知道联系人的设备(如果你们有 <a href="#experiments">通话</a>),或一起使用 <a href="#webxdc">Webxdc应用程序</a></p>
<p>The used <a href="#relays">relays</a> need to know your IP Address,
as well as sometimes your contacts devices if you have a <a href="#calls">call</a>
or use <a href="#webxdc">apps</a> together.</p>
<p>IP 地址是连接和提高效率所必需的。
它们既不会持久存在,也不会暴露。
请注意,IP 地址
不像你给快递服务的详细地址、
而是更粗略,通常只定义地区或国家。</p>
<p>IP Addresses are needed for connectivity and efficiency.
Delta Chat neither persists nor exposes them.
Note that IP Addresses
are not like an address you give to a delivery service,
but typically less precise, often defining city or region only.</p>
<p>这只是互联网和其他信使的默认工作方式、
我们在此不提供选项,也不预先提问。</p>
<p>如果你认为你的 IP 地址存在安全或隐私风险、
我们建议使用 VPN 并结合系统锁定模式。
在系统的所有应用程序中查找选项会留下漏洞。
例如,点击链接会将 IP 地址暴露给未知方,这是目前最大的风险。</p>
<p>If you see your IP Address as a risk,
we recommend to use a VPN for the whole system.
Per-app options leave gaps across your system.
For example, tapping a link can expose IP Addresses to unknown parties, which is by far the larger risk.</p>
<p>###Delta Chat 是否支持 “密封发件人”?{#sealedsender}</p>
+77 -16
View File
@@ -443,10 +443,19 @@ public class Rpc {
}
/**
* Estimate the number of messages that will be deleted
* by the set_config()-options `delete_device_after` or `delete_server_after`.
* Estimates the number of messages that will be deleted
* by the `set_config()`-option `delete_device_after`.
* <p>
* This is typically used to show the estimated impact to the user
* before actually enabling deletion of old messages.
* <p>
* Messages in the "Saved Messages" chat are not counted as they will not be deleted automatically.
* <p>
* Parameters:
* - `from_server`: Deprecated, pass `false` here
* - `seconds`: Count messages older than the given number of seconds.
* <p>
* Returns the number of messages that are older than the given number of seconds.
*/
public Integer estimateAutoDeletionCount(Integer accountId, Boolean fromServer, Integer seconds) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "estimate_auto_deletion_count", mapper.valueToTree(accountId), mapper.valueToTree(fromServer), mapper.valueToTree(seconds));
@@ -710,9 +719,6 @@ public class Rpc {
* because the word "channel" already appears a lot in the code,
* which would make it hard to grep for it.
* <p>
* After creation, the chat contains no recipients and is in _unpromoted_ state;
* see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
* <p>
* Returns the created chat's id.
*/
public Integer createBroadcast(Integer accountId, String chatName) throws RpcException {
@@ -913,8 +919,22 @@ public class Rpc {
}
/**
* Returns all messages of a particular chat.
* Get all message IDs belonging to a chat.
* <p>
* The list is already sorted and starts with the oldest message.
* Clients should not try to re-sort the list as this would be an expensive action
* and would result in inconsistencies between clients.
* Note that the messages are not necessarily sorted by their ID or by their displayed timestamp;
* UIs need to handle both the case of descending message IDs
* and of decreasing timestamps.
* <p>
* Optionally, 'daymarkers' added to the ID array may help to
* implement virtual lists.
* <p>
* Parameters:
* <p>
* * chat_id The chat ID of which the messages IDs should be queried.
* * _info_only: Deprecated, pass `false` here.
* * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
* e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
* corresponding (following) day in the local timezone.
@@ -932,6 +952,14 @@ public class Rpc {
return transport.callForResult(new TypeReference<java.util.List<Integer>>(){}, "get_existing_msg_ids", mapper.valueToTree(accountId), mapper.valueToTree(msgIds));
}
/**
* Get all messages belonging to a chat.
* <p>
* Similar to `get_message_ids` / `getMessageIds`,
* see that function for details.
* The difference is that this function here returns a list of `MessageListItem`,
* which is an enum of a message or a daymarker.
*/
public java.util.List<MessageListItem> getMessageListItems(Integer accountId, Integer chatId, Boolean infoOnly, Boolean addDaymarker) throws RpcException {
return transport.callForResult(new TypeReference<java.util.List<MessageListItem>>(){}, "get_message_list_items", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(infoOnly), mapper.valueToTree(addDaymarker));
}
@@ -1181,11 +1209,6 @@ public class Rpc {
return transport.callForResult(new TypeReference<String>(){}, "make_vcard", mapper.valueToTree(accountId), mapper.valueToTree(contacts));
}
/** Sets vCard containing the given contacts to the message draft. */
public void setDraftVcard(Integer accountId, Integer msgId, java.util.List<Integer> contacts) throws RpcException {
transport.call("set_draft_vcard", mapper.valueToTree(accountId), mapper.valueToTree(msgId), mapper.valueToTree(contacts));
}
/**
* Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
* <p>
@@ -1328,10 +1351,47 @@ public class Rpc {
return transport.callForResult(new TypeReference<String>(){}, "get_connectivity_html", mapper.valueToTree(accountId));
}
/**
* Sets current location.
* <p>
* Returns true if location streaming is currently
* enabled and locations should be updated.
* <p>
* Location is represented as latitude and longitude in degrees
* and horizontal accuracy in meters.
*/
public Boolean setLocation(Float latitude, Float longitude, Float accuracy) throws RpcException {
return transport.callForResult(new TypeReference<Boolean>(){}, "set_location", mapper.valueToTree(latitude), mapper.valueToTree(longitude), mapper.valueToTree(accuracy));
}
public java.util.List<Location> getLocations(Integer accountId, Integer chatId, Integer contactId, Integer timestampBegin, Integer timestampEnd) throws RpcException {
return transport.callForResult(new TypeReference<java.util.List<Location>>(){}, "get_locations", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(contactId), mapper.valueToTree(timestampBegin), mapper.valueToTree(timestampEnd));
}
/**
* Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
* <p>
* Pass 0 as the number of seconds to disable location streaming in the chat.
*/
public void sendLocationsToChat(Integer accountId, Integer chatId, Integer seconds) throws RpcException {
transport.call("send_locations_to_chat", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(seconds));
}
/** Returns whether any chat is sending locations. */
public Boolean isSendingLocations(Integer accountId) throws RpcException {
return transport.callForResult(new TypeReference<Boolean>(){}, "is_sending_locations", mapper.valueToTree(accountId));
}
/** Returns whether `chat_id` is sending locations. */
public Boolean isSendingLocationsToChat(Integer accountId, Integer chatId) throws RpcException {
return transport.callForResult(new TypeReference<Boolean>(){}, "is_sending_locations_to_chat", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
}
/** Stops sending locations to all chats. */
public void stopSendingLocations() throws RpcException {
transport.call("stop_sending_locations");
}
public void sendWebxdcStatusUpdate(Integer accountId, Integer instanceMsgId, String updateStr, String descr) throws RpcException {
transport.call("send_webxdc_status_update", mapper.valueToTree(accountId), mapper.valueToTree(instanceMsgId), mapper.valueToTree(updateStr), mapper.valueToTree(descr));
}
@@ -1467,17 +1527,18 @@ public class Rpc {
transport.call("resend_messages", mapper.valueToTree(accountId), mapper.valueToTree(messageIds));
}
/** @deprecated as of 2026-04; use `send_msg` with `Viewtype::Sticker` instead. */
public Integer sendSticker(Integer accountId, Integer chatId, String stickerPath) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "send_sticker", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(stickerPath));
}
/**
* Send a reaction to message.
* Sends a reaction to message.
* <p>
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
* A reaction is a string that represents an emoji.
* You can call this function again to change the emoji;
* the last sent reaction overrides all previously sent reactions.
* It is possible to remove the reaction by sending an empty string.
*/
public Integer sendReaction(Integer accountId, Integer messageId, java.util.List<String> reaction) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "send_reaction", mapper.valueToTree(accountId), mapper.valueToTree(messageId), mapper.valueToTree(reaction));
@@ -12,6 +12,13 @@ public class EnteredLoginParam {
/** TLS options: whether to allow invalid certificates and/or invalid hostnames. Default: Automatic */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public EnteredCertificateChecks certificateChecks;
/**
* IMAP server folder.
* <p>
* Defaults to "INBOX" if not set. Should not be an empty string.
*/
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String imapFolder;
/** Imap server port. */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public Integer imapPort;
@@ -5,6 +5,6 @@ package chat.delta.rpc.types;
public class Reactions {
/** Unique reactions and their count, sorted in descending order. */
public java.util.List<Reaction> reactions;
/** Map from a contact to it's reaction to message. */
/** Map from a contact to it's reaction to message. There is only a single reaction per contact, but this contains a list of reactions for historical reasons. */
public java.util.Map<String, java.util.List<String>> reactionsByContact;
}
@@ -14,7 +14,7 @@ public enum Viewtype {
Gif,
/**
* Message containing a sticker, similar to image. NB: When sending, the message viewtype may be changed to `Image` by some heuristics like checking for transparent pixels. Use `Message::force_sticker()` to disable them.
* Message containing a sticker, similar to image.
* <p>
* If possible, the ui should display the image without borders in a transparent way. A click on a sticker will offer to install the sticker set in some future.
*/
@@ -2,6 +2,8 @@
package chat.delta.rpc.types;
public class WebxdcMessageInfo {
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String orientation;
/** if the Webxdc represents a document, then this is the name of the document */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String document;
@@ -15,6 +17,10 @@ public class WebxdcMessageInfo {
public String icon;
/** True if full internet access should be granted to the app. */
public Boolean internetAccess;
/** Define if the local user is the one who initially shared the webxdc application in the chat. */
public Boolean isAppSender;
/** Define if the app runs in a broadcasting context. */
public Boolean isBroadcast;
/**
* The name of the app.
* <p>
@@ -84,7 +84,7 @@ public class DcAccounts {
public boolean isAllChatmail() {
for (int accountId : getAll()) {
DcContext dcContext = getAccount(accountId);
if (!dcContext.isChatmail()) {
if (dcContext.getConfigInt("is_chatmail") == 0) {
return false;
}
}
@@ -362,10 +362,6 @@ public class DcContext {
return displayname;
}
public boolean isChatmail() {
return getConfigInt("is_chatmail") == 1;
}
public boolean isMuted() {
return getConfigInt("is_muted") == 1;
}
@@ -194,8 +194,6 @@ public class DcMsg {
public native void setHtml(String text);
public native void forceSticker();
public native void setFileAndDeduplicate(String file, String name, String filemime);
public native void setDimension(int width, int height);
@@ -4,7 +4,6 @@ import android.content.ComponentName;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@@ -12,18 +11,19 @@ import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
@@ -55,7 +55,6 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
this.type3 = type3;
}
}
;
private DcContext dcContext;
private int chatId;
@@ -64,7 +63,7 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
private final ArrayList<TabData> tabs = new ArrayList<>();
private Toolbar toolbar;
private TabLayout tabLayout;
private ViewPager viewPager;
private ViewPager2 viewPager;
private @Nullable MediaController mediaController;
private ListenableFuture<MediaController> mediaControllerFuture;
@@ -98,8 +97,20 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
isGlobalGallery() ? R.string.menu_all_media : R.string.apps_and_media);
}
this.tabLayout.setupWithViewPager(viewPager);
this.viewPager.setAdapter(new AllMediaPagerAdapter(getSupportFragmentManager()));
AllMediaPagerAdapter adapter = new AllMediaPagerAdapter(this);
this.viewPager.setAdapter(adapter);
this.viewPager.registerOnPageChangeCallback(
new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
adapter.onPageChanged(position);
}
});
new TabLayoutMediator(
this.tabLayout,
this.viewPager,
(tab, position) -> tab.setText(getString(tabs.get(position).title)))
.attach();
if (getIntent().getBooleanExtra(FORCE_GALLERY, false)) {
this.viewPager.setCurrentItem(1, false);
}
@@ -186,31 +197,16 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
return contactId == 0 && chatId == 0;
}
private class AllMediaPagerAdapter extends FragmentStatePagerAdapter {
private Object currentFragment = null;
private class AllMediaPagerAdapter extends FragmentStateAdapter {
private int currentPosition = -1;
AllMediaPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
super.setPrimaryItem(container, position, object);
if (currentFragment != null && currentFragment != object) {
ActionMode action = null;
if (currentFragment instanceof MessageSelectorFragment) {
action = ((MessageSelectorFragment) currentFragment).getActionMode();
}
if (action != null) {
action.finish();
}
}
currentFragment = object;
AllMediaPagerAdapter(FragmentActivity activity) {
super(activity);
}
@NonNull
@Override
public Fragment getItem(int position) {
public Fragment createFragment(int position) {
TabData data = tabs.get(position);
Fragment fragment;
Bundle args = new Bundle();
@@ -233,13 +229,24 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
}
@Override
public int getCount() {
public int getItemCount() {
return tabs.size();
}
@Override
public CharSequence getPageTitle(int position) {
return getString(tabs.get(position).title);
private void onPageChanged(int newPosition) {
if (currentPosition != -1 && currentPosition != newPosition) {
for (Fragment fragment : getSupportFragmentManager().getFragments()) {
if (!(fragment instanceof MessageSelectorFragment)) {
continue;
}
ActionMode action = ((MessageSelectorFragment) fragment).getActionMode();
if (action != null) {
action.finish();
}
}
}
currentPosition = newPosition;
}
}
@@ -255,12 +255,6 @@ public class ApplicationContext extends MultiDexApplication {
// 2025-12-16: The setting was removed.
// Revert it to the default if it was changed in the past.
ac.setConfigInt("webxdc_realtime_enabled", 1);
// 2025-11-12: this is needed until core starts ignoring "delete_server_after" for
// chatmail
if (ac.isChatmail()) {
ac.setConfig("delete_server_after", null); // reset
}
}
if (allAccounts.length == 0) {
try {
@@ -308,7 +302,10 @@ public class ApplicationContext extends MultiDexApplication {
Log.i(
"DeltaChat",
"++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
getDcAccounts().maybeNetwork();
// onBlockedStatusChanged is only available on API 29+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getDcAccounts().maybeNetwork();
}
}
@Override
@@ -316,8 +313,13 @@ public class ApplicationContext extends MultiDexApplication {
@NonNull android.net.Network network, boolean blocked) {
Log.i(
"DeltaChat",
"++++++++++++++++++ NetworkCallback.onBlockedStatusChanged() #"
"++++++++++++++++++ NetworkCallback.onBlockedStatusChanged("
+ blocked
+ ") #"
+ debugOnBlockedStatusChangedCount++);
if (!blocked) {
getDcAccounts().maybeNetwork();
}
}
@Override
@@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.connect.DirectShareUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.thoughtcrime.securesms.util.views.ProgressDialog;
@@ -423,9 +422,18 @@ public abstract class BaseConversationListFragment extends Fragment implements A
.setIcon(IconCompat.createWithAdaptiveBitmap(avatar))
.setIntent(intent)
.build();
boolean success;
try {
success = ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfoCompat, null);
} catch (Exception e) {
Log.e(TAG, "ErrAddToHomescreen: requestPinShortcut() failed", e);
success = false;
}
boolean finalSuccess = success;
Util.runOnMain(
() -> {
if (!ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfoCompat, null)) {
if (!finalSuccess) {
Toast.makeText(
activity,
"ErrAddToHomescreen: requestPinShortcut() failed",
@@ -479,10 +487,6 @@ public abstract class BaseConversationListFragment extends Fragment implements A
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
if (isRelayingMessageContent(getActivity())) {
if (ShareUtil.getSharedContactId(getActivity()) != 0) {
return false; // no sharing of a contact to multiple recipients at the same time, we can
// reconsider when that becomes a real-world need
}
Context context = getContext();
if (context != null) {
fab.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_send_sms_white_24dp));
@@ -168,6 +168,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int RECORD_VIDEO = 8;
private static final int PICK_WEBXDC = 9;
private static final Object searchLock = new Object();
private GlideRequests glideRequests;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
@@ -176,7 +178,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected ConversationTitleView titleView;
private ConversationFragment fragment;
private InputAwareLayout container;
private View composePanel;
private ScaleStableImageView backgroundView;
private MessageRequestsBottomView messageRequestBottomView;
private ProgressDialog progressDialog;
@@ -200,7 +201,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private final boolean isSecureText = true;
private boolean isDefaultSms = true;
private boolean isSecurityInitialized = false;
private boolean successfulForwardingAttempt = false;
private boolean isEditing = false;
private boolean switchedProfile = false;
@@ -264,6 +264,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void handleOnBackPressed() {
if (container.isInputOpen()) {
container.hideCurrentInput(composeText);
} else if (searchMenu != null) {
searchCollapse();
} else {
handleReturnToConversationList();
}
@@ -280,6 +282,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
eventCenter.removeObservers(this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_DELETED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
@@ -387,7 +390,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected void onPause() {
super.onPause();
processComposeControls(ACTION_SAVE_DRAFT);
if (inputPanel.isRecording() && inputPanel.getRecordingDuration() > 1000) {
saveRecording();
} else {
processComposeControls(ACTION_SAVE_DRAFT);
}
DcHelper.getNotificationCenter(this).clearVisibleChat();
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
@@ -440,32 +447,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
else mediaType = MediaType.IMAGE;
setMedia(singleUri, mediaType);
} else {
final ClipData multipleUris = data.getClipData();
if (multipleUris != null) {
final int uriCount = multipleUris.getItemCount();
if (uriCount > 0) {
ArrayList<Uri> uriList = new ArrayList<>(uriCount);
for (int i = 0; i < uriCount; i++) {
uriList.add(multipleUris.getItemAt(i).getUri());
}
askSendingFiles(
uriList,
() -> {
Util.runOnAnyBackgroundThread(
() -> {
SendRelayedMessageUtil.sendMultipleMsgs(this, chatId, uriList, null);
});
});
}
}
sendMultipleMsgs(data);
}
break;
case PICK_DOCUMENT:
final String docMimeType = MediaUtil.getMimeType(this, data.getData());
final MediaType docMediaType =
MediaUtil.isAudioType(docMimeType) ? MediaType.AUDIO : MediaType.DOCUMENT;
setMedia(data.getData(), docMediaType);
if (data.getData() != null) { // single Uri
final String docMimeType = MediaUtil.getMimeType(this, data.getData());
final MediaType docMediaType =
MediaUtil.isAudioType(docMimeType) ? MediaType.AUDIO : MediaType.DOCUMENT;
setMedia(data.getData(), docMediaType);
} else {
sendMultipleMsgs(data);
}
break;
case PICK_WEBXDC:
@@ -508,6 +502,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void sendMultipleMsgs(Intent data) {
final ClipData multipleUris = data.getClipData();
if (multipleUris != null) {
final int uriCount = multipleUris.getItemCount();
if (uriCount > 0) {
ArrayList<Uri> uriList = new ArrayList<>(uriCount);
for (int i = 0; i < uriCount; i++) {
uriList.add(multipleUris.getItemAt(i).getUri());
}
askSendingFiles(
uriList,
() -> {
Util.runOnAnyBackgroundThread(
() -> SendRelayedMessageUtil.sendMultipleMsgs(this, chatId, uriList, null));
});
}
}
}
@Override
public void startActivity(Intent intent) {
if (intent.getStringExtra(Browser.EXTRA_APPLICATION_ID) != null) {
@@ -636,12 +649,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return true;
} else if (itemId == R.id.menu_start_audio_call) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CallUtil.startAudioCall(context, chatId);
CallUtil.startAudioCall(this, chatId);
}
return true;
} else if (itemId == R.id.menu_start_video_call) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CallUtil.startVideoCall(context, chatId);
CallUtil.startVideoCall(this, chatId);
}
return true;
} else if (itemId == R.id.menu_all_media) {
@@ -713,7 +726,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
}
playbackViewModel.stopNonMessageAudioPlayback();
if (attachmentManager.isAttachmentPresent()) {
SlideDeck slideDeck = attachmentManager.buildSlideDeck();
int audioDraftId = slideDeck.getAudioDraftId();
if (audioDraftId != 0) {
playbackViewModel.stop(audioDraftId);
}
}
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
Intent intent =
@@ -846,20 +865,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (dcChat.isSelfTalk()) {
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
} else {
String name = dcChat.getName();
if (!dcChat.isMultiUser()) {
int[] contactIds = dcContext.getChatContacts(chatId);
if (contactIds.length == 1 || contactIds.length == 2) {
name = dcContext.getContact(contactIds[0]).getDisplayName();
}
}
int messageIds[] = ShareUtil.getForwardedMessageIDs(this);
int messageCount = messageIds == null ? 0 : messageIds.length;
new AlertDialog.Builder(this)
.setMessage(getString(R.string.ask_forward, name))
.setMessage(
getResources()
.getQuantityString(
R.plurals.ask_forward_messages, messageCount, messageCount, dcChat.getName()))
.setPositiveButton(
R.string.ok,
R.string.forward,
(dialogInterface, i) -> {
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
successfulForwardingAttempt = true;
})
.setNegativeButton(R.string.cancel, (dialogInterface, i) -> finish())
.setOnCancelListener(dialog -> finish())
@@ -883,13 +899,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void handleSharing() {
ArrayList<Uri> uriList = ShareUtil.getSharedUris(this);
int sharedContactId = ShareUtil.getSharedContactId(this);
if (uriList.size() > 1) {
askSendingFiles(uriList, () -> SendRelayedMessageUtil.immediatelyRelay(this, chatId));
} else {
if (sharedContactId != 0) {
addAttachmentContactInfo(sharedContactId);
} else if (ShareUtil.getSharedHtml(this) != null
if (ShareUtil.getSharedHtml(this) != null
|| ShareUtil.getSharedSubject(this) != null
|| ("sticker".equals(ShareUtil.getSharedType(this)) && !uriList.isEmpty())) {
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
@@ -977,6 +990,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
setMedia(draft, MediaType.GIF).addListener(listener);
break;
case DcMsg.DC_MSG_AUDIO:
case DcMsg.DC_MSG_VOICE:
setMedia(draft, MediaType.AUDIO).addListener(listener);
break;
case DcMsg.DC_MSG_VIDEO:
@@ -1017,7 +1031,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
attachButton = ViewUtil.findById(this, R.id.attach_button);
composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
emojiPickerContainer = ViewUtil.findById(this, R.id.emoji_picker_container);
composePanel = ViewUtil.findById(this, R.id.bottom_panel);
container = ViewUtil.findById(this, R.id.layout_container);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
@@ -1043,7 +1056,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// apply padding top to avoid drawing behind top bar
ViewUtil.applyWindowInsets(findViewById(R.id.fragment_content), false, true, false, false);
// apply padding to root to avoid collision with system bars
ViewUtil.applyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
ViewUtil.applyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
ViewUtil.applyWindowInsets(emojiPickerContainer, false, false, false, true);
container.addOnKeyboardShownListener(this);
container.addOnKeyboardHiddenListener(backgroundView);
@@ -1133,22 +1147,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipient = new Recipient(this, dcChat);
glideRequests = GlideApp.with(this);
setComposePanelVisibility(true);
setInputPanelVisibility(true);
initializeContactRequest();
}
private void setComposePanelVisibility(boolean isInitialization) {
private void setInputPanelVisibility(boolean isInitialization) {
int inputPanelVisibility;
boolean isAttachmentHidden;
if (dcChat.canSend()) {
composePanel.setVisibility(View.VISIBLE);
attachmentManager.setHidden(false);
inputPanelVisibility = View.VISIBLE;
isAttachmentHidden = false;
inputPanel.setSubjectVisible(!dcChat.isEncrypted());
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input
// bar
// ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
// fragment.handleRemoveBottomInsets();
} else {
composePanel.setVisibility(View.GONE);
attachmentManager.setHidden(true);
inputPanelVisibility = View.GONE;
isAttachmentHidden = true;
hideSoftKeyboard();
inputPanel.setSubjectVisible(false);
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input
@@ -1160,6 +1176,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
*/
}
synchronized (searchLock) {
if (searchMenu != null) { // in search mode, don't change visibility directly
beforeSearchInputPanelVisibility = inputPanelVisibility;
beforeSearchAttachmentEditorHidden = isAttachmentHidden;
} else {
inputPanel.setVisibility(inputPanelVisibility);
// attachmentManager.setHidden(isAttachmentHidden);
}
}
}
//////// Helper Methods
@@ -1272,9 +1297,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inputPanel.clearSubject();
}
// Stop draft audio playback regardless, since it is unlikely
// we will need background playback for drafts
playbackViewModel.stopNonMessageAudioPlayback();
// Stop draft audio playback
if (slideDeck != null) {
int audioDraftId = slideDeck.getAudioDraftId();
if (audioDraftId != 0) {
playbackViewModel.stop(audioDraftId);
}
}
DcContext dcContext = DcHelper.getContext(context);
final int currentChatId = dcChat.getId();
@@ -1624,7 +1653,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
msg.setQuote(quote.get().getQuotedMsg());
}
msg.setFileAndDeduplicate(path, null, null);
msg.forceSticker();
dcContext.sendMsg(chatId, msg);
}
@@ -1686,6 +1714,56 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void saveRecording() {
inputPanel.resetRecordingUI();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
final int thisChatId = chatId;
final Optional<QuoteModel> quote = inputPanel.getQuote();
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(
new ListenableFuture.Listener<Pair<Uri, Long>>() {
@Override
public void onSuccess(final @NonNull Pair<Uri, Long> result) {
Util.runOnAnyBackgroundThread(
() -> {
try {
DcContext dcContext = DcHelper.getContext(context);
String path =
DcHelper.copyToBlobdir(
ConversationActivity.this, result.first, "voice", ".m4a");
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_VOICE);
msg.setFileAndDeduplicate(path, null, null);
if (quote.isPresent()) {
msg.setQuote(quote.get().getQuotedMsg());
}
dcContext.setDraft(thisChatId, msg);
} catch (Exception e) {
Log.e(TAG, "Failed to save voice as draft", e);
} finally {
PersistentBlobProvider.getInstance()
.delete(ConversationActivity.this, result.first);
}
runOnUiThread(
() -> {
if (chatId == thisChatId && !isFinishing() && !isDestroyed()) {
initializeDraft();
updateToggleButtonState();
}
});
});
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, "Failed to stop recording", e);
}
});
}
private class AttachButtonListener implements OnClickListener {
@Override
public void onClick(View v) {
@@ -1800,19 +1878,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
dcChat = dcContext.getChat(chatId);
titleView.setTitle(glideRequests, dcChat);
initializeSecurity(isSecureText, isDefaultSms);
setComposePanelVisibility(false);
setInputPanelVisibility(false);
initializeContactRequest();
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|| eventId == DcContext.DC_EVENT_MSG_READ)
&& event.getData1Int() == chatId) {
dcChat = dcContext.getChat(chatId);
titleView.setTitle(glideRequests, dcChat);
} else if (eventId == DcContext.DC_EVENT_CHAT_DELETED && event.getData1Int() == chatId) {
finish();
}
}
// in-chat search
private int beforeSearchComposeVisibility = View.VISIBLE;
private boolean beforeSearchAttachmentEditorHidden;
private int beforeSearchMsgRequestVisibility;
private int beforeSearchInputPanelVisibility;
private Menu searchMenu = null;
private int[] searchResult = {};
@@ -1833,17 +1915,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void searchExpand(final Menu menu, final MenuItem searchItem) {
searchMenu = menu;
synchronized (searchLock) {
searchMenu = menu;
beforeSearchComposeVisibility = composePanel.getVisibility();
composePanel.setVisibility(View.GONE);
beforeSearchAttachmentEditorHidden = attachmentManager.isHidden();
beforeSearchMsgRequestVisibility = messageRequestBottomView.getVisibility();
beforeSearchInputPanelVisibility = inputPanel.getVisibility();
// attachmentManager.setHidden(true);
messageRequestBottomView.setVisibility(View.GONE);
inputPanel.setVisibility(View.GONE);
}
ConversationActivity.this.makeSearchMenuVisible(menu, searchItem);
}
private void searchCollapse() {
searchMenu = null;
composePanel.setVisibility(beforeSearchComposeVisibility);
synchronized (searchLock) {
searchMenu = null;
// attachmentManager.setHidden(beforeSearchAttachmentEditorHidden);
messageRequestBottomView.setVisibility(beforeSearchMsgRequestVisibility);
inputPanel.setVisibility(beforeSearchInputPanelVisibility);
}
// trigger onPrepareOptionsMenu() to restore correct menu visibility
invalidateOptionsMenu();
@@ -1904,16 +1997,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void initializeContactRequest() {
if (!dcChat.isContactRequest()) {
messageRequestBottomView.setVisibility(View.GONE);
synchronized (searchLock) {
if (searchMenu != null) { // in search mode, don't change visibility directly
beforeSearchMsgRequestVisibility = View.GONE;
} else {
messageRequestBottomView.setVisibility(View.GONE);
}
}
return;
}
messageRequestBottomView.setVisibility(View.VISIBLE);
synchronized (searchLock) {
if (searchMenu != null) { // in search mode, don't change visibility directly
beforeSearchMsgRequestVisibility = View.VISIBLE;
} else {
messageRequestBottomView.setVisibility(View.VISIBLE);
}
}
messageRequestBottomView.setAcceptOnClickListener(
v -> {
DcHelper.getContext(context).acceptChat(chatId);
messageRequestBottomView.setVisibility(View.GONE);
composePanel.setVisibility(View.VISIBLE);
inputPanel.setVisibility(View.VISIBLE);
});
if (dcChat.getType() == DcChat.DC_CHAT_TYPE_GROUP) {
@@ -462,7 +462,7 @@ public class ConversationItem extends BaseConversationItem {
int end = spanned.getSpanEnd(span);
if (start >= 0 && end > start && end <= spanned.length()) {
String linkText = spanned.subSequence(start, end).toString();
String label = context.getString(R.string.accessibility_link_action, linkText);
String label = context.getString(R.string.open_link, linkText);
linkActionIds.add(
ViewCompat.addAccessibilityAction(
this,
@@ -1096,7 +1096,18 @@ public class ConversationItem extends BaseConversationItem {
if (!messageRecord.isOutgoing() && callInfo.state instanceof CallState.Alerting) {
int callId = messageRecord.getId();
CallCoordinator coordinator = CallCoordinator.getInstance(context);
coordinator.showIncomingCallScreen(callId);
if (coordinator.hasActiveCall()) {
coordinator.showIncomingCallScreen(callId);
} else {
if (callInfo.sdpOffer == null) {
Toast.makeText(context, R.string.error, Toast.LENGTH_SHORT).show();
return;
}
int accId = dcContext.getAccountId();
coordinator.handleIncomingCallFromConversation(
accId, callId, callInfo.sdpOffer, callInfo.hasVideo);
}
} else {
if (callInfo.hasVideo) {
CallUtil.startVideoCall(getContext(), chatId);
@@ -607,6 +607,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
}
}
public void handleQrFromSearch(String rawQrString) {
qrData = rawQrString;
new QrCodeHandler(this)
.handleQrData(
rawQrString, SecurejoinSource.Scan, SecurejoinUiPath.QrIcon, relayLockLauncher);
}
private void handleResetRelaying() {
resetRelayingMessageContent(this);
refreshTitle();
@@ -720,6 +727,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
refreshAvatar();
refreshUnreadIndicator();
refreshTitle();
conversationListFragment.resetScrollPosition();
conversationListFragment.loadChatlistAsync();
}
@@ -23,7 +23,6 @@ import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -61,11 +60,11 @@ public class ConversationListFragment extends BaseConversationListFragment
private RecyclerView list;
private View emptyState;
private TextView emptySearch;
private final String queryFilter = "";
private boolean archive;
private Timer reloadTimer;
private boolean chatlistJustLoaded;
private boolean reloadTimerInstantly;
private boolean resetScrollPosition;
@Override
public void onCreate(Bundle icicle) {
@@ -242,9 +241,9 @@ public class ConversationListFragment extends BaseConversationListFragment
public void loadChatlistAsync() {
synchronized (loadChatlistLock) {
needsAnotherLoad = true;
if (inLoadChatlist) {
Log.i(TAG, "chatlist loading debounced");
needsAnotherLoad = true;
return;
}
inLoadChatlist = true;
@@ -253,6 +252,9 @@ public class ConversationListFragment extends BaseConversationListFragment
Util.runOnAnyBackgroundThread(
() -> {
while (true) {
Log.i(TAG, "executing debounced chatlist loading");
loadChatlist();
synchronized (loadChatlistLock) {
if (!needsAnotherLoad) {
inLoadChatlist = false;
@@ -261,8 +263,6 @@ public class ConversationListFragment extends BaseConversationListFragment
needsAnotherLoad = false;
}
Log.i(TAG, "executing debounced chatlist loading");
loadChatlist();
Util.sleep(100);
}
});
@@ -285,22 +285,17 @@ public class ConversationListFragment extends BaseConversationListFragment
Log.w(TAG, "Ignoring call to loadChatlist()");
return;
}
DcChatlist chatlist =
DcHelper.getContext(context)
.getChatlist(listflags, queryFilter.isEmpty() ? null : queryFilter, 0);
long startMs = System.currentTimeMillis();
DcChatlist chatlist = DcHelper.getContext(context).getChatlist(listflags, null, 0);
Log.i(TAG, "⏰ getChatlist(): " + (System.currentTimeMillis() - startMs) + "ms");
Util.runOnMain(
() -> {
if (chatlist.getCnt() <= 0 && TextUtils.isEmpty(queryFilter)) {
if (chatlist.getCnt() <= 0) {
list.setVisibility(View.INVISIBLE);
emptyState.setVisibility(View.VISIBLE);
emptySearch.setVisibility(View.INVISIBLE);
fab.startPulse(3 * 1000);
} else if (chatlist.getCnt() <= 0 && !TextUtils.isEmpty(queryFilter)) {
list.setVisibility(View.INVISIBLE);
emptyState.setVisibility(View.GONE);
emptySearch.setVisibility(View.VISIBLE);
emptySearch.setText(getString(R.string.search_no_result_for_x, queryFilter));
} else {
list.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
@@ -309,6 +304,11 @@ public class ConversationListFragment extends BaseConversationListFragment
}
((ConversationListAdapter) list.getAdapter()).changeData(chatlist);
if (resetScrollPosition) {
list.scrollToPosition(0);
resetScrollPosition = false;
}
});
}
@@ -365,4 +365,8 @@ public class ConversationListFragment extends BaseConversationListFragment
loadChatlistAsync();
}
}
public void resetScrollPosition() {
resetScrollPosition = true;
}
}
@@ -44,9 +44,11 @@ import org.thoughtcrime.securesms.components.AvatarView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.search.QrInviteData;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -222,6 +224,40 @@ public class ConversationListItem extends RelativeLayout
avatar.setSeenRecently(false);
}
public void bind(@NonNull QrInviteData inviteData, @NonNull GlideRequests glideRequests) {
this.selectedThreads = Collections.emptySet();
fromView.setText(inviteData.getDisplayTitle());
fromView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
subjectView.setVisibility(VISIBLE);
subjectView.setText(inviteData.getDisplaySubtitle());
subjectView.setTypeface(LIGHT_TYPEFACE);
subjectView.setTextColor(
ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color));
dateView.setText("");
dateView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
archivedBadgeView.setVisibility(GONE);
requestBadgeView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
deliveryStatusIndicator.setNone();
setBatchState(false);
if (inviteData.getContactId() > 0) {
DcContext dcContext = DcHelper.getContext(getContext());
DcContact contact = dcContext.getContact(inviteData.getContactId());
Recipient recipient = new Recipient(getContext(), contact);
avatar.setAvatar(glideRequests, recipient, false);
avatar.setSeenRecently(contact.wasSeenRecently());
} else {
avatar.setImageDrawable(
new GeneratedContactPhoto("+")
.asDrawable(getContext(), ThemeUtil.getDummyContactColor(getContext())));
avatar.setSeenRecently(false);
}
}
@Override
public void unbind() {}
@@ -33,14 +33,13 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMediaGalleryElement;
@@ -48,7 +47,6 @@ import com.b44t.messenger.DcMsg;
import java.io.IOException;
import java.util.WeakHashMap;
import org.thoughtcrime.securesms.components.MediaView;
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
@@ -88,7 +86,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
@Nullable private DcMsg messageRecord;
private DcContext dcContext;
private MediaItem initialMedia;
private ViewPager mediaPager;
private ViewPager2 mediaPager;
private Recipient conversationRecipient;
private boolean leftIsRecent;
@@ -190,7 +188,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
private void initializeViews() {
mediaPager = findViewById(R.id.media_pager);
mediaPager.setOffscreenPageLimit(1);
mediaPager.addOnPageChangeListener(new ViewPagerListener());
mediaPager.registerOnPageChangeCallback(new ViewPagerListener());
}
private void initializeResources() {
@@ -260,10 +258,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
private int cleanupMedia() {
int restartItem = mediaPager.getCurrentItem();
mediaPager.removeAllViews();
mediaPager.setAdapter(null);
return restartItem;
}
@@ -483,22 +478,25 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
@SuppressWarnings("ConstantConditions")
DcMediaPagerAdapter adapter =
new DcMediaPagerAdapter(this, GlideApp.with(this), getWindow(), data, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
mediaPager.setAdapter(adapter);
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition());
else mediaPager.setCurrentItem(restartItem);
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition(), false);
else mediaPager.setCurrentItem(restartItem, false);
}
}
@Override
public void onLoaderReset(Loader<DcMediaGalleryElement> loader) {}
private class ViewPagerListener extends ExtendedOnPageChangedListener {
private class ViewPagerListener extends ViewPager2.OnPageChangeCallback {
private Integer currentPage = null;
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
if (currentPage != null && currentPage != position) onPageUnselected(currentPage);
currentPage = position;
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
@@ -510,8 +508,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
}
}
@Override
public void onPageUnselected(int position) {
private void onPageUnselected(int position) {
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
if (adapter != null) {
@@ -526,7 +523,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
}
}
private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter {
private static class SingleItemPagerAdapter
extends RecyclerView.Adapter<SingleItemPagerAdapter.MediaViewHolder>
implements MediaItemAdapter {
private final GlideRequests glideRequests;
private final Window window;
@@ -555,37 +554,29 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
}
@Override
public int getCount() {
public int getItemCount() {
return 1;
}
@NonNull
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new MediaViewHolder(inflater.inflate(R.layout.media_view_page, parent, false));
}
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
View itemView = inflater.inflate(R.layout.media_view_page, container, false);
MediaView mediaView = itemView.findViewById(R.id.media_view);
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
try {
mediaView.set(glideRequests, window, uri, name, mediaType, size, true);
holder.mediaView.set(glideRequests, window, uri, name, mediaType, size, true);
} catch (IOException e) {
Log.w(TAG, e);
}
container.addView(itemView);
return itemView;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
mediaView.cleanup();
container.removeView((FrameLayout) object);
public void onViewRecycled(@NonNull MediaViewHolder holder) {
super.onViewRecycled(holder);
holder.mediaView.cleanup();
}
@Override
@@ -595,9 +586,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
@Override
public void pause(int position) {}
static class MediaViewHolder extends RecyclerView.ViewHolder {
final MediaView mediaView;
MediaViewHolder(@NonNull View itemView) {
super(itemView);
mediaView = itemView.findViewById(R.id.media_view);
}
}
}
private static class DcMediaPagerAdapter extends PagerAdapter implements MediaItemAdapter {
private static class DcMediaPagerAdapter
extends RecyclerView.Adapter<DcMediaPagerAdapter.MediaViewHolder>
implements MediaItemAdapter {
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
@@ -630,21 +632,20 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
}
@Override
public int getCount() {
public int getItemCount() {
if (!active) return 0;
else return gallery.getCount();
}
@NonNull
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(context).inflate(R.layout.media_view_page, parent, false);
return new MediaViewHolder(itemView);
}
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
View itemView =
LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false);
MediaView mediaView = itemView.findViewById(R.id.media_view);
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
boolean autoplay = position == autoPlayPosition;
int cursorPosition = getCursorPosition(position);
@@ -656,7 +657,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
try {
//noinspection ConstantConditions
mediaView.set(
holder.mediaView.set(
glideRequests,
window,
Uri.fromFile(msg.getFileAsFile()),
@@ -668,19 +669,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
Log.w(TAG, e);
}
mediaViews.put(position, mediaView);
container.addView(itemView);
return itemView;
mediaViews.put(position, holder.mediaView);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
mediaView.cleanup();
mediaViews.remove(position);
container.removeView((FrameLayout) object);
public void onViewRecycled(@NonNull MediaViewHolder holder) {
super.onViewRecycled(holder);
int pos = holder.getBindingAdapterPosition();
if (pos != RecyclerView.NO_POSITION) {
mediaViews.remove(pos);
}
holder.mediaView.cleanup();
}
public MediaItem getMediaItemFor(int position) {
@@ -710,6 +709,15 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
if (leftIsRecent) return position;
else return gallery.getCount() - 1 - position;
}
static class MediaViewHolder extends RecyclerView.ViewHolder {
final MediaView mediaView;
MediaViewHolder(@NonNull View itemView) {
super(itemView);
mediaView = itemView.findViewById(R.id.media_view);
}
}
}
private static class MediaItem {
@@ -742,7 +750,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
}
}
interface MediaItemAdapter {
private interface MediaItemAdapter {
MediaItem getMediaItemFor(int position);
void pause(int position);
@@ -7,6 +7,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
@@ -25,8 +26,11 @@ import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ShareUtil;
@@ -36,6 +40,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
public class ProfileActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate {
private static final String TAG = "ProfileActivity";
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
@@ -398,7 +404,18 @@ public class ProfileActivity extends PassphraseRequiredActionBarActivity
Intent composeIntent = new Intent();
DcContact dcContact = dcContext.getContact(contactId);
if (dcContact.isKeyContact()) {
ShareUtil.setSharedContactId(composeIntent, contactId);
try {
byte[] vcard =
rpc.makeVcard(rpc.getSelectedAccountId(), Collections.singletonList(contactId))
.getBytes();
Uri vcardUri =
PersistentBlobProvider.getInstance().create(this, vcard, "text/vcard", "contact.vcf");
ArrayList<Uri> uris = new ArrayList<>();
uris.add(vcardUri);
ShareUtil.setSharedUris(composeIntent, uris);
} catch (RpcException e) {
Log.e(TAG, "Failed to create vCard for sharing contactId=" + contactId, e);
}
} else {
ShareUtil.setSharedText(composeIntent, dcContact.getAddr());
}
@@ -260,12 +260,11 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
final String addr = extraEmail[0];
int contactId = dcContext.lookupContactIdByAddr(addr);
if (contactId == 0) {
contactId = dcContext.createContact(null, addr);
if (contactId != 0 || dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
if (contactId == 0) contactId = dcContext.createContact(null, addr);
chatId = dcContext.createChatByContactId(contactId);
accId = dcContext.getAccountId();
}
chatId = dcContext.createChatByContactId(contactId);
accId = dcContext.getAccountId();
}
Intent composeIntent;
if (accId != -1 && chatId > 0) {
@@ -68,6 +68,11 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
}
}
@Override
protected boolean allowInLockedMode() {
return true;
}
@Override
protected void onCreate(Bundle state, boolean ready) {
setContentView(R.layout.web_view_activity);
@@ -34,6 +34,7 @@ import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.WebxdcMessageInfo;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
@@ -50,7 +51,6 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.json.JSONObject;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.IntentUtils;
@@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.util.Util;
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
private static final String TAG = "WebxdcActivity";
private static final String EXTRA_ACCOUNT_ID = "accountId";
private static final String EXTRA_CHAT_ID = "chatId";
private static final String EXTRA_APP_MSG_ID = "appMessageId";
private static final String EXTRA_HIDE_ACTION_BAR = "hideActionBar";
private static final String EXTRA_HREF = "href";
@@ -69,11 +70,15 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
private ValueCallback<Uri[]> filePathCallback;
private DcContext dcContext;
private int accountId;
private Rpc rpc;
private int chatId;
private DcMsg dcAppMsg;
private String baseURL;
private String sourceCodeUrl = "";
private String selfAddr;
private boolean isAppSender;
private boolean isBroadcast;
private int sendUpdateMaxSize;
private int sendUpdateInterval;
private boolean internetAccess = false;
@@ -109,7 +114,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
dcContext.setConfigInt("ui.maps_version", mapsVersion);
}
openWebxdcActivity(context, msgId, true, href);
openWebxdcActivity(context, msgId, chatId, true, href);
}
public static void openWebxdcActivity(Context context, DcMsg instance) {
@@ -117,37 +122,43 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
public static void openWebxdcActivity(Context context, @NonNull DcMsg instance, String href) {
openWebxdcActivity(context, instance.getId(), false, href);
openWebxdcActivity(context, instance.getId(), instance.getChatId(), false, href);
}
public static void openWebxdcActivity(
Context context, int msgId, boolean hideActionBar, String href) {
Context context, int msgId, int chatId, boolean hideActionBar, String href) {
if (!Util.isClickedRecently()) {
context.startActivity(getWebxdcIntent(context, msgId, hideActionBar, href));
context.startActivity(getWebxdcIntent(context, msgId, chatId, hideActionBar, href));
}
}
private static Intent getWebxdcIntent(
Context context, int msgId, boolean hideActionBar, String href) {
Context context, int msgId, int chatId, boolean hideActionBar, String href) {
DcContext dcContext = DcHelper.getContext(context);
int accountId = dcContext.getAccountId();
Intent intent = new Intent(context, WebxdcActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(EXTRA_ACCOUNT_ID, dcContext.getAccountId());
// Unique URI per webxdc instance so FLAG_ACTIVITY_NEW_DOCUMENT can identify the document:
intent.setData(Uri.parse("webxdc://" + accountId + "/" + msgId));
intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
intent.putExtra(EXTRA_CHAT_ID, chatId);
intent.putExtra(EXTRA_APP_MSG_ID, msgId);
intent.putExtra(EXTRA_HIDE_ACTION_BAR, hideActionBar);
intent.putExtra(EXTRA_HREF, href);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
return intent;
}
private static Intent[] getWebxdcIntentWithParentStack(Context context, int msgId) {
DcContext dcContext = DcHelper.getContext(context);
int chatId = dcContext.getMsg(msgId).getChatId();
final Intent chatIntent =
new Intent(context, ConversationActivity.class)
.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId())
.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId)
.setAction(Intent.ACTION_VIEW);
final Intent webxdcIntent = getWebxdcIntent(context, msgId, false, "");
final Intent webxdcIntent = getWebxdcIntent(context, msgId, chatId, false, "");
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(chatIntent)
@@ -193,18 +204,11 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
});
DcEventCenter eventCenter =
DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this);
int appMessageId = b.getInt(EXTRA_APP_MSG_ID);
int accountId = b.getInt(EXTRA_ACCOUNT_ID);
accountId = b.getInt(EXTRA_ACCOUNT_ID);
this.dcContext = DcHelper.getContext(getApplicationContext());
if (accountId != dcContext.getAccountId()) {
AccountManager.getInstance().switchAccount(getApplicationContext(), accountId);
this.dcContext = DcHelper.getContext(getApplicationContext());
this.dcContext = DcHelper.getAccounts(getApplicationContext()).getAccount(accountId);
}
this.dcAppMsg = this.dcContext.getMsg(appMessageId);
@@ -213,6 +217,15 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
finish();
return;
}
chatId = b.getInt(EXTRA_CHAT_ID, dcAppMsg.getChatId());
DcEventCenter eventCenter =
DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext());
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_MSG_DELETED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_DELETED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this);
// `msg_id` in the subdomain makes sure, different apps using same files do not share the same
// cache entry
@@ -220,21 +233,32 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
// (a random-id would also work, but would need maintenance and does not add benefits as we
// regard the file-part interceptRequest() only,
// also a random-id is not that useful for debugging)
this.baseURL = "https://acc" + dcContext.getAccountId() + "-msg" + appMessageId + ".localhost";
this.baseURL = "https://acc" + accountId + "-msg" + appMessageId + ".localhost";
final JSONObject info = this.dcAppMsg.getWebxdcInfo();
internetAccess = JsonUtils.optBoolean(info, "internet_access");
if ("landscape".equals(JsonUtils.optString(info, "orientation"))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
// enter fullscreen mode if necessary,
// this is needed here because if the app is opened while already in landscape mode,
// onConfigurationChanged() is not triggered
setScreenMode(getResources().getConfiguration());
WebxdcMessageInfo info;
try {
info = rpc.getWebxdcInfo(accountId, appMessageId);
if ("landscape".equals(info.orientation)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
// enter fullscreen mode if necessary,
// this is needed here because if the app is opened while already in landscape mode,
// onConfigurationChanged() is not triggered
setScreenMode(getResources().getConfiguration());
}
internetAccess = info.internetAccess;
selfAddr = info.selfAddr;
isAppSender = info.isAppSender;
isBroadcast = info.isBroadcast;
sendUpdateMaxSize = info.sendUpdateMaxSize;
sendUpdateInterval = info.sendUpdateInterval;
} catch (RpcException e) { // unexpected error, log it and finish
Log.e(TAG, "RPC Error", e);
finish();
return;
}
selfAddr = info.optString("self_addr");
sendUpdateMaxSize = info.optInt("send_update_max_size");
sendUpdateInterval = info.optInt("send_update_interval");
toggleFakeProxy(!internetAccess);
@@ -315,7 +339,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.menu_add_to_home_screen) {
addToHomeScreen(this, dcAppMsg.getId());
addToHomeScreen(this, dcContext, dcAppMsg.getId());
return true;
} else if (itemId == R.id.webxdc_help) {
DcHelper.openHelp(this, "#webxdc");
@@ -468,6 +492,8 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
@Override
public void handleEvent(@NonNull DcEvent event) {
if (event.getAccountId() != accountId) return;
int eventId = event.getId();
if ((eventId == DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE
&& event.getData1Int() == dcAppMsg.getId())) {
@@ -487,22 +513,26 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
this.dcContext.getMsg(event.getData2Int()); // msg changed, reload data from db
Util.runOnAnyBackgroundThread(
() -> {
final JSONObject info = dcAppMsg.getWebxdcInfo();
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Util.runOnMain(
() -> {
updateTitleAndMenu(info, chat);
});
try {
final WebxdcMessageInfo info =
rpc.getWebxdcInfo(dcContext.getAccountId(), dcAppMsg.getId());
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Util.runOnMain(() -> updateTitleAndMenu(info, chat));
} catch (RpcException e) {
Log.e(TAG, "RPC Error", e);
}
});
} else if ((eventId == DcContext.DC_EVENT_MSG_DELETED
&& event.getData2Int() == dcAppMsg.getId())
|| (eventId == DcContext.DC_EVENT_CHAT_DELETED && event.getData1Int() == chatId)) {
finish();
}
}
private void updateTitleAndMenu(JSONObject info, DcChat chat) {
final String docName = JsonUtils.optString(info, "document");
final String xdcName = JsonUtils.optString(info, "name");
final String currSourceCodeUrl = JsonUtils.optString(info, "source_code_url");
getSupportActionBar()
.setTitle((docName.isEmpty() ? xdcName : docName) + " " + chat.getName());
private void updateTitleAndMenu(WebxdcMessageInfo info, DcChat chat) {
final String docName = TextUtils.isEmpty(info.document) ? info.name : info.document;
getSupportActionBar().setTitle(docName + " " + chat.getName());
String currSourceCodeUrl = info.sourceCodeUrl != null ? info.sourceCodeUrl : "";
if (!sourceCodeUrl.equals(currSourceCodeUrl)) {
sourceCodeUrl = currSourceCodeUrl;
invalidateOptionsMenu();
@@ -511,6 +541,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
private void showInChat() {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accountId);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcAppMsg.getChatId());
intent.putExtra(
ConversationActivity.STARTING_POSITION_EXTRA,
@@ -519,35 +550,53 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
}
public static void addToHomeScreen(Activity activity, int msgId) {
addToHomeScreen(activity, DcHelper.getContext(activity), msgId);
}
public static void addToHomeScreen(Activity activity, DcContext dcContext, int msgId) {
Context context = activity.getApplicationContext();
try {
DcContext dcContext = DcHelper.getContext(context);
Rpc rpc = DcHelper.getRpc(context);
int accountId = dcContext.getAccountId();
DcMsg msg = dcContext.getMsg(msgId);
final JSONObject info = msg.getWebxdcInfo();
WebxdcMessageInfo info = rpc.getWebxdcInfo(accountId, msgId);
final String docName = JsonUtils.optString(info, "document");
final String xdcName = JsonUtils.optString(info, "name");
byte[] blob = msg.getWebxdcBlob(JsonUtils.optString(info, "icon"));
final String docName = TextUtils.isEmpty(info.document) ? info.name : info.document;
byte[] blob = msg.getWebxdcBlob(info.icon);
ByteArrayInputStream is = new ByteArrayInputStream(blob);
BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon");
Bitmap bitmap = drawable.getBitmap();
ShortcutInfoCompat shortcutInfoCompat =
new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId)
.setShortLabel(docName.isEmpty() ? xdcName : docName)
new ShortcutInfoCompat.Builder(context, "xdc-" + accountId + "-" + msgId)
.setShortLabel(docName)
.setIcon(
IconCompat.createWithBitmap(
bitmap)) // createWithAdaptiveBitmap() removes decorations but cuts out a too
// small circle and defamiliarize the icon too much
.setIntents(getWebxdcIntentWithParentStack(context, msgId))
.setIntent(getWebxdcIntent(context, msgId, msg.getChatId(), false, ""))
.build();
Toast.makeText(context, R.string.one_moment, Toast.LENGTH_SHORT).show();
if (!ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) {
Toast.makeText(
context, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG)
.show();
}
Util.runOnAnyBackgroundThread(
() -> {
boolean success;
try {
success = ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null);
} catch (Exception e) {
Log.e(TAG, "ErrAddToHomescreen: requestPinShortcut() failed", e);
success = false;
}
if (!success) {
Util.runOnMain(
() ->
Toast.makeText(
context,
"ErrAddToHomescreen: requestPinShortcut() failed",
Toast.LENGTH_LONG)
.show());
}
});
} catch (Exception e) {
Toast.makeText(context, "ErrAddToHomescreen: " + e, Toast.LENGTH_LONG).show();
}
@@ -617,6 +666,16 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
return WebxdcActivity.this.dcContext.getName();
}
@JavascriptInterface
public boolean isAppSender() {
return WebxdcActivity.this.isAppSender;
}
@JavascriptInterface
public boolean isBroadcast() {
return WebxdcActivity.this.isBroadcast;
}
/**
* @noinspection unused
*/
@@ -1,10 +1,14 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -71,6 +75,11 @@ public class WelcomeActivity extends BaseActionBarActivity
.setOnClickListener((v) -> showSignInDialogWithPermission());
findViewById(R.id.backup_button).setOnClickListener((v) -> startImportBackup());
AnimatorSet floating =
(AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.floating_logo);
floating.setTarget(findViewById(R.id.welcome_icon));
floating.start();
registerForEvents();
initializeActionBar();
@@ -120,7 +129,15 @@ public class WelcomeActivity extends BaseActionBarActivity
boolean canGoBack = AccountManager.getInstance().canRollbackAccountCreation(this);
supportActionBar.setDisplayHomeAsUpEnabled(canGoBack);
getSupportActionBar().setTitle(canGoBack ? R.string.add_account : R.string.app_name);
if (canGoBack) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
supportActionBar.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
supportActionBar.setElevation(0);
}
supportActionBar.setTitle(R.string.add_account);
} else {
supportActionBar.hide();
}
}
private void registerForEvents() {
@@ -189,7 +206,12 @@ public class WelcomeActivity extends BaseActionBarActivity
File imexDir = DcHelper.getImexDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AttachmentManager.selectMediaType(
this, "application/x-tar", null, PICK_BACKUP, StorageUtil.getDownloadUri());
this,
"application/x-tar",
null,
PICK_BACKUP,
StorageUtil.getDownloadUri(),
false);
} else {
final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath());
if (backupFile != null) {
@@ -4,15 +4,10 @@ import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
public abstract class Attachment {
@@ -116,14 +111,7 @@ public abstract class Attachment {
filename = filename.substring(0, i);
}
}
String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext);
// copy content to this file
InputStream inputStream = PartAuthority.getAttachmentStream(context, getDataUri());
OutputStream outputStream = new FileOutputStream(path);
Util.copy(inputStream, outputStream);
return path;
return DcHelper.copyToBlobdir(context, getDataUri(), filename, ext);
} catch (Exception e) {
e.printStackTrace();
return null;
@@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.calls;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationManagerCompat;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.R;
@RequiresApi(api = Build.VERSION_CODES.O)
public class CallActionReceiver extends BroadcastReceiver {
private static final String TAG = "CallActionReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) return;
String action = intent.getAction();
Log.d(TAG, "Received action: " + action);
if (CallActivity.ACTION_DECLINE_CALL.equals(action)) {
CallCoordinator.getInstance(context).declineCall();
} else if (CallActivity.ACTION_HANGUP_CALL.equals(action)) {
CallCoordinator.getInstance(context).hangUp();
} else if (CallActivity.ACTION_CALL_BACK.equals(action)) {
int chatId = intent.getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
int accId = intent.getIntExtra(ConversationActivity.ACCOUNT_ID_EXTRA, -1);
boolean video = intent.getBooleanExtra(CallActivity.EXTRA_STARTS_WITH_VIDEO, false);
if (chatId > 0 && accId > 0) {
NotificationManagerCompat.from(context).cancel(CallCoordinator.NOTIFICATION_ID_MISSED_CALL);
CallCoordinator coordinator = CallCoordinator.getInstance(context);
if (coordinator.hasActiveCall()) {
Toast.makeText(context, R.string.already_in_call, Toast.LENGTH_SHORT).show();
} else {
coordinator.initiateOutgoingCall(accId, chatId, video);
}
}
} else if (CallActivity.ACTION_MESSAGE.equals(action)) {
int chatId = intent.getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
int accId = intent.getIntExtra(ConversationActivity.ACCOUNT_ID_EXTRA, -1);
NotificationManagerCompat.from(context).cancel(CallCoordinator.NOTIFICATION_ID_MISSED_CALL);
if (chatId > 0 && accId > 0) {
Intent convIntent = new Intent(context, ConversationActivity.class);
convIntent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
convIntent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
convIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(convIntent);
}
}
}
}
@@ -6,11 +6,13 @@ import android.app.PictureInPictureParams;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import android.util.Rational;
import android.view.View;
@@ -22,7 +24,9 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import androidx.constraintlayout.widget.ConstraintLayout;
@@ -54,10 +58,14 @@ public class CallActivity extends AppCompatActivity {
private static final String TAG = "CallActivity";
private static final int MIC_PERMISSION_REQUEST_CODE = 1001;
private static final int CAMERA_PERMISSION_REQUEST_CODE = 1002;
private static final int CAMERA_MID_CALL_PERMISSION_REQUEST_CODE = 1003;
public static final String ACTION_ANSWER_CALL = BuildConfig.APPLICATION_ID + ".ANSWER_CALL";
public static final String ACTION_DECLINE_CALL = BuildConfig.APPLICATION_ID + ".DECLINE_CALL";
public static final String ACTION_HANGUP_CALL = BuildConfig.APPLICATION_ID + ".HANGUP_CALL";
public static final String ACTION_CALL_BACK = BuildConfig.APPLICATION_ID + ".CALL_BACK";
public static final String ACTION_MESSAGE = BuildConfig.APPLICATION_ID + ".MESSAGE";
public static final String EXTRA_STARTS_WITH_VIDEO = "starts_with_video";
// Views
@@ -99,6 +107,7 @@ public class CallActivity extends AppCompatActivity {
private boolean awaitingPermissionResult = false;
private boolean pausedWhileAwaitingPermission = false;
private boolean intentHandled = false;
private boolean doNotAutoFinish = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -227,6 +236,7 @@ public class CallActivity extends AppCompatActivity {
Log.d(TAG, "Resuming existing call");
} else if (!coordinator.isIncomingCall()) {
Log.d(TAG, "Starting outgoing call");
coordinator.ensureServiceStarted();
viewModel.startOutgoingCallWhenReady();
}
}
@@ -374,6 +384,18 @@ public class CallActivity extends AppCompatActivity {
videoButton.setOnClickListener(
v -> {
if (viewModel != null) {
Boolean currentEnabled = viewModel.getVideoEnabled().getValue();
boolean needToEnable = currentEnabled == null || !currentEnabled;
if (needToEnable && !hasCameraPermission()) {
awaitingPermissionResult = true;
ActivityCompat.requestPermissions(
this,
new String[] {Manifest.permission.CAMERA},
CAMERA_MID_CALL_PERMISSION_REQUEST_CODE);
return;
}
viewModel.toggleVideo();
}
});
@@ -634,7 +656,7 @@ public class CallActivity extends AppCompatActivity {
new Handler(Looper.getMainLooper())
.postDelayed(
() -> {
if (!isFinishing()) {
if (!isFinishing() && !doNotAutoFinish) {
finish();
}
},
@@ -643,7 +665,9 @@ public class CallActivity extends AppCompatActivity {
case ENDED:
statusText.setText(R.string.call_ended);
finish();
if (!doNotAutoFinish) {
finish();
}
break;
case ERROR:
@@ -656,7 +680,7 @@ public class CallActivity extends AppCompatActivity {
new Handler(Looper.getMainLooper())
.postDelayed(
() -> {
if (!isFinishing()) {
if (!isFinishing() && !doNotAutoFinish) {
finish();
}
},
@@ -842,16 +866,28 @@ public class CallActivity extends AppCompatActivity {
}
private void handleMicPermissionDenied() {
Toast.makeText(this, R.string.call_requires_mic_permission, Toast.LENGTH_LONG).show();
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
if (coordinator.hasActiveCall()
&& coordinator.isIncomingCall()
&& !coordinator.hasOngoingCall()) {
coordinator.declineCall();
if (coordinator.hasActiveCall() && !coordinator.hasOngoingCall()) {
if (coordinator.isIncomingCall()) {
coordinator.declineCall();
} else {
coordinator.hangUp();
}
}
finish();
if (!shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
doNotAutoFinish = true;
showPermissionSettingsDialog(
getString(R.string.call_requires_mic_permission),
() -> {
doNotAutoFinish = false;
if (!isFinishing()) finish();
});
} else {
Toast.makeText(this, R.string.call_requires_mic_permission, Toast.LENGTH_LONG).show();
finish();
}
}
private void proceedAfterPermissions() {
@@ -878,6 +914,22 @@ public class CallActivity extends AppCompatActivity {
CallCoordinator coordinator = CallCoordinator.getInstance(getApplication());
if (requestCode == CAMERA_MID_CALL_PERMISSION_REQUEST_CODE) {
boolean cameraGranted =
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (cameraGranted && viewModel != null) {
viewModel.toggleVideo();
} else {
if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
showPermissionSettingsDialog(getString(R.string.call_requires_camera_permission), null);
} else {
Toast.makeText(this, R.string.call_requires_camera_permission, Toast.LENGTH_SHORT).show();
}
}
return;
}
if (requestCode == MIC_PERMISSION_REQUEST_CODE) {
boolean micGranted =
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
@@ -893,9 +945,17 @@ public class CallActivity extends AppCompatActivity {
if (!cameraGranted) {
Log.w(TAG, "Camera permission denied, switching to audio-only");
Toast.makeText(
this, "Starting audio-only call (camera permission denied)", Toast.LENGTH_SHORT)
.show();
if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
Toast.makeText(
this,
"Camera permission permanently denied. Enable in app settings for video calls.",
Toast.LENGTH_LONG)
.show();
} else {
Toast.makeText(
this, "Starting audio-only call (camera permission denied)", Toast.LENGTH_SHORT)
.show();
}
coordinator.setStartsWithVideo(false);
}
}
@@ -903,6 +963,34 @@ public class CallActivity extends AppCompatActivity {
proceedAfterPermissions();
}
private void showPermissionSettingsDialog(String message, @Nullable Runnable onDismissAction) {
if (isFinishing() || isDestroyed()) {
if (onDismissAction != null) onDismissAction.run();
return;
}
new AlertDialog.Builder(this)
.setTitle("Permission Required")
.setMessage(message)
.setPositiveButton(
"Open Settings",
(dialog, which) -> {
Intent intent =
new Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", getPackageName(), null));
startActivity(intent);
})
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(
dialog -> {
if (onDismissAction != null) {
onDismissAction.run();
}
})
.show();
}
// Picture-in-Picture
@Override
@@ -23,6 +23,7 @@ import android.os.Looper;
import android.telecom.DisconnectCause;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
@@ -32,6 +33,7 @@ import androidx.core.telecom.CallControlScope;
import androidx.core.telecom.CallEndpointCompat;
import androidx.core.telecom.CallException;
import androidx.core.telecom.CallsManager;
import androidx.core.util.Pair;
import androidx.lifecycle.FlowLiveDataConversions;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
@@ -55,6 +57,7 @@ import kotlinx.coroutines.Dispatchers;
import kotlinx.coroutines.flow.Flow;
import kotlinx.coroutines.flow.FlowKt;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -68,7 +71,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
// Notification channels
private static final String CHANNEL_ID_INCOMING = "voip_incoming_calls";
private static final String CHANNEL_ID_ONGOING = "voip_ongoing_calls";
private static final String CHANNEL_ID_MISSED = "voip_missed_calls";
private static final int NOTIFICATION_ID_CALL = 1001;
static final int NOTIFICATION_ID_MISSED_CALL = 1002;
private static final int PI_ANSWER = 0;
private static final int PI_DECLINE = 1;
private static final int PI_FULLSCREEN = 2;
private static final int PI_HANGUP = 3;
private static final int PI_ONGOING_CONTENT = 4;
private static final int PI_MISSED_CONTENT = 5;
private static final int PI_MISSED_CALLBACK = 6;
private static final int PI_MISSED_MESSAGE = 7;
private static final String CALL_IDENTIFIER_SCHEME = "deltachat:";
@@ -97,6 +111,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
private final MutableLiveData<Boolean> outgoingCallPlaced = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> answeredElsewhere = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> isFrontCamera = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> mediaCaptureReady = new MutableLiveData<>(false);
// Audio Routing Support
private final MediatorLiveData<CallEndpointCompat> currentAudioEndpoint =
@@ -119,6 +134,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
private String pendingOfferSdp;
private boolean hasNotifiedBackend = false;
private boolean hasAutoSelectedEarpiece = false;
private boolean pendingMediaCapture = false;
private boolean wasAnsweredLocally = false;
private CallControlScope activeCallControlScope;
private CallViewModel activeCallViewModel;
@@ -165,8 +182,14 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
ongoingChannel.setDescription("Notifications for active DeltaChat calls");
ongoingChannel.setSound(null, null);
NotificationChannel missedChannel =
new NotificationChannel(
CHANNEL_ID_MISSED, "Missed Calls", NotificationManager.IMPORTANCE_HIGH);
missedChannel.setDescription("Notifications for missed DeltaChat calls");
notificationManager.createNotificationChannel(incomingChannel);
notificationManager.createNotificationChannel(ongoingChannel);
notificationManager.createNotificationChannel(missedChannel);
}
private void registerTelecom() {
@@ -180,6 +203,29 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
}
}
private void disconnectTelecom(DisconnectCause cause) {
CallControlScope scope = activeCallControlScope;
if (scope == null) {
Log.d(TAG, "No active CallControlScope, skipping disconnect");
return;
}
scope.disconnect(
cause,
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
Log.d(TAG, "Telecom disconnect completed: " + result);
}
});
}
private void addEventListeners() {
DcEventCenter eventCenter = DcHelper.getEventCenter(this.appContext);
eventCenter.removeObservers(this);
@@ -228,6 +274,11 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
mainHandler.removeCallbacks(outgoingRingtoneRunnable);
mainHandler.postDelayed(outgoingRingtoneRunnable, 1500);
}
if (pendingMediaCapture) {
pendingMediaCapture = false;
callService.startMediaCapture();
}
}
}
@@ -336,6 +387,10 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return isFrontCamera;
}
public LiveData<Boolean> getMediaCaptureReady() {
return mediaCaptureReady;
}
// State Update Methods (CallService)
public void updateConnectionState(PeerConnection.PeerConnectionState state) {
@@ -375,6 +430,11 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
isFrontCamera.postValue(front);
}
public void updateMediaCaptureReady(boolean ready) {
Log.d(TAG, "updateMediaCaptureReady: " + ready);
mediaCaptureReady.postValue(ready);
}
public void reportError(String error) {
Log.e(TAG, "reportError: " + error);
errorMessage.postValue(error);
@@ -387,7 +447,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
if (callService != null) {
callService.startMediaCapture();
} else {
Log.w(TAG, "Cannot start media capture, service not ready");
Log.d(TAG, "Service not ready, deferring media capture");
pendingMediaCapture = true;
}
}
@@ -399,6 +460,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return;
}
wasAnsweredLocally = true;
if (callService != null) {
callService.stopRingtone();
}
@@ -562,28 +625,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
notifyBackendCallEnded();
// Disconnect with CallControlScope
CallControlScope scope = activeCallControlScope;
if (scope != null) {
scope.disconnect(
new DisconnectCause(DisconnectCause.REJECTED),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
if (result instanceof CallControlResult) {
Log.d(TAG, "Decline succeeded with CallControlScope");
} else if (result instanceof kotlin.Result.Failure) {
Log.e(TAG, "Decline failed", ((kotlin.Result.Failure) result).exception);
}
}
});
}
disconnectTelecom(new DisconnectCause(DisconnectCause.REJECTED));
// End call on service
if (callService != null) {
@@ -604,28 +646,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
notifyBackendCallEnded();
// Disconnect with CallControlScope
CallControlScope scope = activeCallControlScope;
if (scope != null) {
scope.disconnect(
new DisconnectCause(DisconnectCause.LOCAL),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
if (result instanceof CallControlResult) {
Log.d(TAG, "Hang up succeeded with CallControlScope");
} else if (result instanceof kotlin.Result.Failure) {
Log.e(TAG, "Hang up failed", ((kotlin.Result.Failure) result).exception);
}
}
});
}
disconnectTelecom(new DisconnectCause(DisconnectCause.LOCAL));
// End call on service
if (callService != null) {
@@ -651,11 +672,17 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
public synchronized void setVideoEnabled(boolean enabled) {
Log.d(TAG, "setVideoEnabled: " + enabled);
if (callService != null) {
boolean success = callService.setVideoEnabled(enabled);
if (!success && enabled) {
enabled = false;
reportError("Camera unavailable");
}
}
localVideoEnabled.postValue(enabled);
if (callService != null) {
callService.setVideoEnabled(enabled);
callService.sendMutedState(Boolean.TRUE.equals(localAudioEnabled.getValue()), enabled);
}
}
@@ -865,7 +892,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
// This event is problematic because it can trigger in both directions,
// in addition to multiple other scenarios which cannot easily be distinguished
// May cause problems in edge cases
onCallEnded(accId, callId);
onCallEnded(accId, callId, startsWithVideo);
break;
}
});
@@ -875,32 +902,15 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
int accId, int callId, String offerSdp, boolean startsWithVideo) {
Log.d(TAG, "onIncomingCall: accId=" + accId + ", callId=" + callId);
if (hasActiveCall()) {
Log.w(TAG, "Already have an active call, ignoring incoming call");
return;
}
Pair<DcChat, String> result = setupIncomingCallState(accId, callId, offerSdp, startsWithVideo);
if (result == null) return;
resetLiveDataForNewCall();
this.activeAccId = accId;
this.activeCallId = callId;
this.isIncomingCall = true;
this.startsWithVideo = startsWithVideo;
this.pendingOfferSdp = offerSdp;
// Get caller info
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accId);
int chatId = dcContext.getMsg(callId).getChatId();
this.activeChatId = chatId;
DcChat dcChat = dcContext.getChat(chatId);
String callerName = getNameFromChat(dcChat);
DcChat dcChat = result.first;
String callerName = result.second;
Icon callerIcon = getIconFromChat(this.appContext, dcChat);
displayName.postValue(callerName);
displayIcon.postValue(callerIcon);
this.preferredStartingEndpoint = getPreferredStartingEndpoint(startsWithVideo);
// Add to CallsManager
CallAttributesCompat callAttributes = createCallAttributes(callerName, callId, true);
addCallToTelecom(callAttributes, callerName, callerIcon);
@@ -911,6 +921,37 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
startAndBindService();
}
public synchronized void handleIncomingCallFromConversation(
int accId, int callId, String offerSdp, boolean hasVideo) {
Log.d(TAG, "handleIncomingCallFromConversation: accId=" + accId + ", callId=" + callId);
if (offerSdp == null || offerSdp.isEmpty()) {
Log.e(TAG, "Cannot start incoming call: no SDP offer");
return;
}
Pair<DcChat, String> result = setupIncomingCallState(accId, callId, offerSdp, hasVideo);
if (result == null) return;
DcChat dcChat = result.first;
String callerName = result.second;
new Thread(
() -> {
Icon callerIcon = getIconFromChat(this.appContext, dcChat);
displayIcon.postValue(callerIcon);
})
.start();
// Add to CallsManager
CallAttributesCompat callAttributes = createCallAttributes(callerName, callId, true);
addCallToTelecom(callAttributes, callerName, null);
startAndBindService();
launchCallActivity();
}
private synchronized void onIncomingCallAccepted(int callId, boolean fromThisDevice) {
Log.d(TAG, "onIncomingCallAccepted: callId=" + callId + ", fromThisDevice=" + fromThisDevice);
@@ -952,24 +993,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
answeredElsewhere.postValue(true);
// Disconnect from Telecom CallControlScope
CallControlScope scope = activeCallControlScope;
if (scope != null) {
scope.disconnect(
new DisconnectCause(DisconnectCause.REMOTE),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
Log.d(TAG, "Disconnect (answered elsewhere) completed");
}
});
}
disconnectTelecom(new DisconnectCause(DisconnectCause.REMOTE));
if (callService != null) {
callService.endCall();
@@ -1019,7 +1043,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
showOrUpdateOngoingNotification(appContext.getString(R.string.call_with, calleeName));
}
private synchronized void onCallEnded(int accId, int callId) {
private synchronized void onCallEnded(int accId, int callId, boolean startsWithVideo) {
Log.d(TAG, "onCallEnded: accId=" + accId + ", callId=" + callId);
if (!hasActiveCall()) {
@@ -1047,30 +1071,16 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
callService.stopRingtone();
}
// Disconnect from CallControlScope
if (activeCallControlScope != null) {
activeCallControlScope.disconnect(
// We actually don't know if this is incoming or outgoing
// But we have to provide one of LOCAL, REMOTE, MISSED, REJECTED
new DisconnectCause(DisconnectCause.REMOTE),
new Continuation<CallControlResult>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NonNull Object result) {
Log.d(TAG, "Disconnect completed");
}
});
}
disconnectTelecom(new DisconnectCause(DisconnectCause.REMOTE));
if (callService != null) {
callService.endCall();
}
if (isIncomingCall && !wasAnsweredLocally) {
showMissedCallNotification(activeAccId, activeChatId, startsWithVideo);
}
// Clear active states
cleanupCall(accId, callId);
}
@@ -1089,6 +1099,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
notifyBackendCallEnded();
DisconnectCause cause;
if (state == PeerConnection.PeerConnectionState.FAILED) {
cause = new DisconnectCause(DisconnectCause.REMOTE, "PeerConnection failed");
} else {
cause = new DisconnectCause(DisconnectCause.LOCAL, "PeerConnection closed");
}
disconnectTelecom(cause);
if (callService != null) {
callService.endCall();
}
// Cleanup
if (hasActiveCall()) {
cleanupCall(activeAccId, activeCallId);
@@ -1129,6 +1151,8 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
this.pendingOfferSdp = null;
this.hasNotifiedBackend = false;
this.hasAutoSelectedEarpiece = false;
this.pendingMediaCapture = false;
this.wasAnsweredLocally = false;
mainHandler.removeCallbacks(outgoingRingtoneRunnable);
@@ -1194,6 +1218,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
private void resetLiveDataForNewCall() {
connectionState.postValue(PeerConnection.PeerConnectionState.NEW);
answeredElsewhere.postValue(false); // clearLiveData() must not reset answeredElsewhere
mediaCaptureReady.postValue(false);
clearLiveData();
}
@@ -1209,6 +1234,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
outgoingCallPlaced.postValue(false);
currentAudioEndpoint.postValue(null);
availableAudioEndpoints.postValue(null);
mediaCaptureReady.postValue(false);
}
public synchronized void initiateOutgoingCall(int accId, int chatId, boolean startsWithVideo) {
@@ -1219,16 +1245,6 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
return;
}
// Check microphone permission
if (!hasMicrophonePermission()) {
Log.e(TAG, "Microphone permission not granted");
Intent intent = new Intent(appContext, CallActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
appContext.startActivity(intent);
return;
}
resetLiveDataForNewCall();
this.activeCallId = -1; // Placeholder call ID for Intent
@@ -1254,7 +1270,9 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
this.preferredStartingEndpoint = getPreferredStartingEndpoint(startsWithVideo);
startAndBindService();
if (hasMicrophonePermission()) {
startAndBindService();
}
launchCallActivity();
}
@@ -1286,6 +1304,43 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
addCallToTelecom(callAttributes, calleeName, calleeIcon);
}
@Nullable
private Pair<DcChat, String> setupIncomingCallState(
int accId, int callId, String offerSdp, boolean startsWithVideo) {
if (hasActiveCall()) {
Log.w(TAG, "Already have an active call, ignoring incoming call");
return null;
}
resetLiveDataForNewCall();
this.activeAccId = accId;
this.activeCallId = callId;
this.isIncomingCall = true;
this.startsWithVideo = startsWithVideo;
this.pendingOfferSdp = offerSdp;
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accId);
int chatId = dcContext.getMsg(callId).getChatId();
this.activeChatId = chatId;
DcChat dcChat = dcContext.getChat(chatId);
String callerName = getNameFromChat(dcChat);
displayName.postValue(callerName);
this.preferredStartingEndpoint = getPreferredStartingEndpoint(startsWithVideo);
return new Pair<>(dcChat, callerName);
}
public synchronized void ensureServiceStarted() {
if (isServiceBound || !hasActiveCall()) {
return;
}
Log.d(TAG, "Starting service after permission grant");
startAndBindService();
}
private void addCallToTelecom(
CallAttributesCompat callAttributes, String displayName, Icon icon) {
try {
@@ -1388,19 +1443,18 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
PendingIntent answerPendingIntent =
PendingIntent.getActivity(
this.appContext,
0,
PI_ANSWER,
answerIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// Decline intent
Intent declineIntent = new Intent(this.appContext, CallActivity.class);
Intent declineIntent = new Intent(this.appContext, CallActionReceiver.class);
declineIntent.setAction(CallActivity.ACTION_DECLINE_CALL);
declineIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent declinePendingIntent =
PendingIntent.getActivity(
PendingIntent.getBroadcast(
this.appContext,
1,
PI_DECLINE,
declineIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
@@ -1411,7 +1465,7 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
PendingIntent fullScreenPendingIntent =
PendingIntent.getActivity(
this.appContext,
2,
PI_FULLSCREEN,
fullScreenIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
@@ -1462,26 +1516,106 @@ public class CallCoordinator implements DcEventCenter.DcEventDelegate {
notificationManager.notify(NOTIFICATION_ID_CALL, builder.build());
}
private void showMissedCallNotification(int accId, int chatId, boolean wasVideoCall) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (!hasNotificationPermission()) {
Log.w(TAG, "Cannot show missed call notification: no permission");
return;
}
}
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accId);
DcChat dcChat = dcContext.getChat(chatId);
String callerName = CallUtil.getNameFromChat(dcChat);
Intent contentAction = new Intent(appContext, ConversationActivity.class);
contentAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
contentAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
contentAction.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent contentIntent =
PendingIntent.getActivity(
appContext,
PI_MISSED_CONTENT,
contentAction,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Intent callBackAction = new Intent(appContext, CallActionReceiver.class);
callBackAction.setAction(CallActivity.ACTION_CALL_BACK);
callBackAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
callBackAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
callBackAction.putExtra(CallActivity.EXTRA_STARTS_WITH_VIDEO, wasVideoCall);
PendingIntent callBackIntent =
PendingIntent.getBroadcast(
appContext,
PI_MISSED_CALLBACK,
callBackAction,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Intent messageAction = new Intent(appContext, CallActionReceiver.class);
messageAction.setAction(CallActivity.ACTION_MESSAGE);
messageAction.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
messageAction.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, accId);
PendingIntent messageIntent =
PendingIntent.getBroadcast(
appContext,
PI_MISSED_MESSAGE,
messageAction,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
String contentText = appContext.getString(R.string.missed_call);
Notification.Builder builder =
new Notification.Builder(appContext, CHANNEL_ID_MISSED)
.setSmallIcon(R.drawable.icon_notification)
.setContentTitle(callerName)
.setContentText(contentText)
.setContentIntent(contentIntent)
.setAutoCancel(true)
.addAction(
new Notification.Action.Builder(
null, appContext.getString(R.string.call_back), callBackIntent)
.build())
.addAction(
new Notification.Action.Builder(
null, appContext.getString(R.string.chat_input_placeholder), messageIntent)
.build());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setCategory(Notification.CATEGORY_MISSED_CALL);
} else {
builder.setCategory(Notification.CATEGORY_CALL);
}
Icon icon = displayIcon.getValue();
if (icon != null) {
builder.setLargeIcon(icon);
}
notificationManager.notify(NOTIFICATION_ID_MISSED_CALL, builder.build());
}
private Notification buildOngoingCallNotification(
String statusText, String displayName, Icon icon) {
Intent activityIntent = new Intent(this.appContext, CallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
Intent hangupIntent = new Intent(this.appContext, CallActivity.class);
Intent hangupIntent = new Intent(this.appContext, CallActionReceiver.class);
hangupIntent.setAction(CallActivity.ACTION_HANGUP_CALL);
hangupIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent hangupPendingIntent =
PendingIntent.getActivity(
PendingIntent.getBroadcast(
this.appContext,
3,
PI_HANGUP,
hangupIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
PendingIntent contentIntent =
PendingIntent.getActivity(
this.appContext,
4,
PI_ONGOING_CONTENT,
activityIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
@@ -126,13 +126,13 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
}
/**
* Start camera/microphone capture
* Start media capture
*
* <p>Must be called when app is in foreground. Called by coordinator when ViewModel/Activity is
* ready.
*/
public void startMediaCapture() {
Log.d(TAG, "startMediaCapture (Camera/Microphone)");
Log.d(TAG, "startMediaCapture");
if (webRTCClient != null && webRTCClient.hasLocalMediaStream()) {
Log.w(TAG, "Media already initialized, skipping");
@@ -153,7 +153,7 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
boolean startsWithVideo = callCoordinator.isStartsWithVideo();
Log.d(TAG, "Creating media stream with video: " + startsWithVideo);
Log.d(TAG, "Creating media stream");
mediaStreamManager.createMediaStream(
new MediaStreamManager.Callback() {
@@ -163,31 +163,22 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
webRTCClient.setLocalMediaStream(stream);
callCoordinator.updateFrontCamera(mediaStreamManager.isFrontCamera());
callCoordinator.setVideoEnabled(startsWithVideo);
if (!stream.videoTracks.isEmpty()) {
VideoTrack localTrack = stream.videoTracks.get(0);
callCoordinator.updateLocalVideoTrack(localTrack);
} else {
Log.w(TAG, "No video track in stream, call will be audio-only");
if (startsWithVideo) {
callCoordinator.reportError("Camera unavailable, using audio only");
}
callCoordinator.setVideoEnabled(false);
}
callCoordinator.setVideoEnabled(startsWithVideo);
callCoordinator.updateMediaCaptureReady(true);
Log.d(TAG, "Media capture complete, ready for call");
}
@Override
public void onError(String error) {
Log.e(TAG, "Failed to setup media: " + error);
if (startsWithVideo) {
callCoordinator.reportError("Camera/microphone error: " + error);
}
callCoordinator.setVideoEnabled(false);
callCoordinator.reportError("Camera/microphone error: " + error);
}
});
}
@@ -395,12 +386,30 @@ public class CallService extends Service implements WebRTCClient.Callbacks {
}
}
public void setVideoEnabled(boolean enabled) {
public boolean setVideoEnabled(boolean enabled) {
Log.d(TAG, "setVideoEnabled: " + enabled);
if (webRTCClient != null) {
webRTCClient.setVideoEnabled(enabled);
if (enabled) {
if (mediaStreamManager != null) {
boolean captureReady = mediaStreamManager.startVideoCapture();
if (!captureReady) {
Log.w(TAG, "Failed to start video capture");
return false;
}
callCoordinator.updateFrontCamera(mediaStreamManager.isFrontCamera());
}
if (webRTCClient != null) {
webRTCClient.setVideoEnabled(true);
}
} else {
if (webRTCClient != null) {
webRTCClient.setVideoEnabled(false);
}
if (mediaStreamManager != null) {
mediaStreamManager.stopVideoCapture();
}
}
return true;
}
public void sendMutedState(boolean audioEnabled, boolean videoEnabled) {
@@ -5,13 +5,18 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.core.telecom.CallEndpointCompat;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -52,8 +57,22 @@ public class CallUtil {
return;
}
int accId = DcHelper.getContext(context).getAccountId();
coordinator.initiateOutgoingCall(accId, chatId, startsWithVideo);
Runnable proceedWithCall =
() -> {
int accId = DcHelper.getContext(context).getAccountId();
coordinator.initiateOutgoingCall(accId, chatId, startsWithVideo);
};
if (!isNetworkAvailable(context)) {
new AlertDialog.Builder(context)
.setMessage(context.getString(R.string.call_requires_connection))
.setPositiveButton(R.string.perm_continue, (dialog, which) -> proceedWithCall.run())
.setNegativeButton(android.R.string.cancel, null)
.show();
return;
}
proceedWithCall.run();
}
@Nullable
@@ -138,4 +157,24 @@ public class CallUtil {
}
return iconRes;
}
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isNetworkAvailable(Context context) {
ConnectivityManager manager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (manager == null) return true;
boolean networkAvailable = false;
Network network = manager.getActiveNetwork();
if (network != null) {
NetworkCapabilities caps = manager.getNetworkCapabilities(network);
networkAvailable =
caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
boolean serverConnected =
DcHelper.getContext(context).getConnectivity() >= DcContext.DC_CONNECTIVITY_WORKING;
return networkAvailable || serverConnected;
}
}
@@ -46,8 +46,8 @@ public class CallViewModel extends AndroidViewModel {
private final MediatorLiveData<CallState> callState;
// Observer References for one-time observe
private Observer<VideoTrack> answerCallObserver;
private Observer<VideoTrack> startOutgoingCallObserver;
private Observer<Boolean> answerCallObserver;
private Observer<Boolean> startOutgoingCallObserver;
private final AtomicBoolean hasCallEnded = new AtomicBoolean(false);
@@ -211,25 +211,24 @@ public class CallViewModel extends AndroidViewModel {
callCoordinator.startMediaCapture();
// Create one-time observer
LiveData<VideoTrack> localTrack = callCoordinator.getLocalVideoTrack();
LiveData<Boolean> mediaReady = callCoordinator.getMediaCaptureReady();
answerCallObserver =
new Observer<VideoTrack>() {
new Observer<Boolean>() {
@Override
public void onChanged(VideoTrack videoTrack) {
if (videoTrack != null) {
// Media is ready, remove observer
localTrack.removeObserver(this);
public void onChanged(Boolean ready) {
if (Boolean.TRUE.equals(ready)) {
mediaReady.removeObserver(this);
answerCallObserver = null;
Log.d(TAG, "Local video ready, answering call (WebRTC)");
Log.d(TAG, "Media capture ready, answering call (WebRTC)");
callCoordinator.answerWebRTC();
}
}
};
localTrack.observeForever(answerCallObserver);
mediaReady.observeForever(answerCallObserver);
}
/** Start outgoing call with media capture Called by Activity for outgoing calls */
@@ -244,30 +243,28 @@ public class CallViewModel extends AndroidViewModel {
callCoordinator.startMediaCapture();
// Create one-time observer
LiveData<VideoTrack> localTrack = callCoordinator.getLocalVideoTrack();
VideoTrack currentValue = localTrack.getValue();
LiveData<Boolean> mediaReady = callCoordinator.getMediaCaptureReady();
if (currentValue != null) {
if (Boolean.TRUE.equals(mediaReady.getValue())) {
Log.d(TAG, "Media already ready, starting call immediately");
callCoordinator.startOutgoingCall();
} else {
startOutgoingCallObserver =
new Observer<VideoTrack>() {
new Observer<Boolean>() {
@Override
public void onChanged(VideoTrack videoTrack) {
if (videoTrack != null) {
// Media is ready, remove observer
localTrack.removeObserver(this);
public void onChanged(Boolean ready) {
if (Boolean.TRUE.equals(ready)) {
mediaReady.removeObserver(this);
startOutgoingCallObserver = null;
Log.d(TAG, "Local video ready, starting outgoing call");
Log.d(TAG, "Media capture ready, starting outgoing call");
callCoordinator.startOutgoingCall();
}
}
};
localTrack.observeForever(startOutgoingCallObserver);
mediaReady.observeForever(startOutgoingCallObserver);
}
}
@@ -460,12 +457,12 @@ public class CallViewModel extends AndroidViewModel {
Log.d(TAG, "CallViewModel cleared");
if (answerCallObserver != null) {
callCoordinator.getLocalVideoTrack().removeObserver(answerCallObserver);
callCoordinator.getMediaCaptureReady().removeObserver(answerCallObserver);
answerCallObserver = null;
}
if (startOutgoingCallObserver != null) {
callCoordinator.getLocalVideoTrack().removeObserver(startOutgoingCallObserver);
callCoordinator.getMediaCaptureReady().removeObserver(startOutgoingCallObserver);
startOutgoingCallObserver = null;
}
@@ -9,10 +9,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import java.util.List;
import org.thoughtcrime.securesms.EglUtils;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerationAndroid;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
@@ -22,6 +24,7 @@ import org.webrtc.VideoCapturer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
@RequiresApi(Build.VERSION_CODES.M)
public class MediaStreamManager {
private static final String TAG = "MediaStreamManager";
@@ -29,6 +32,10 @@ public class MediaStreamManager {
private static final String AUDIO_TRACK_ID = "audio_track";
private static final String VIDEO_TRACK_ID = "video_track";
private static final int TARGET_WIDTH = 1280;
private static final int TARGET_HEIGHT = 720;
private static final int TARGET_FPS = 30;
private final Context context;
private final PeerConnectionFactory peerConnectionFactory;
@@ -37,6 +44,10 @@ public class MediaStreamManager {
private AudioSource audioSource;
private SurfaceTextureHelper surfaceTextureHelper;
private volatile boolean isFrontCamera = true;
private volatile boolean isCapturing = false;
private volatile String currentDeviceName;
private volatile int currentCaptureWidth;
private volatile int currentCaptureHeight;
public interface Callback {
void onMediaStreamReady(MediaStream stream);
@@ -56,9 +67,8 @@ public class MediaStreamManager {
this.peerConnectionFactory = peerConnectionFactory;
}
/** Create media stream with audio and optionally video */
@RequiresApi(api = Build.VERSION_CODES.M)
public void createMediaStream(Callback callback) {
/** Create a media stream with an audio track and a video track. */
public synchronized void createMediaStream(Callback callback) {
try {
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream(STREAM_ID);
@@ -68,24 +78,11 @@ public class MediaStreamManager {
AudioTrack audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
mediaStream.addTrack(audioTrack);
// Create video track
videoCapturer = createVideoCapturer();
if (videoCapturer == null) {
callback.onError("No camera available");
callback.onMediaStreamReady(mediaStream);
return;
}
videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
// Create video source and track
videoSource = peerConnectionFactory.createVideoSource(false);
VideoTrack videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
mediaStream.addTrack(videoTrack);
// Start capturing
surfaceTextureHelper =
SurfaceTextureHelper.create("CaptureThread", EglUtils.getEglBase().getEglBaseContext());
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
videoCapturer.startCapture(1280, 720, 30);
callback.onMediaStreamReady(mediaStream);
} catch (Exception e) {
@@ -94,6 +91,129 @@ public class MediaStreamManager {
}
}
/**
* Open the camera and start sending frames to VideoSource.
*
* @return true if the camera is capturing, false if it could not be started
*/
public synchronized boolean startVideoCapture() {
if (isCapturing) {
return true;
}
if (videoSource == null) {
Log.e(TAG, "VideoSource not initialized");
return false;
}
if (videoCapturer == null) {
videoCapturer = createVideoCapturer();
if (videoCapturer == null) {
Log.w(TAG, "Cannot start video capture: no camera available");
return false;
}
if (surfaceTextureHelper == null) {
surfaceTextureHelper =
SurfaceTextureHelper.create("CaptureThread", EglUtils.getEglBase().getEglBaseContext());
}
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
}
int[] captureFormat = selectCaptureFormat(currentDeviceName);
currentCaptureWidth = captureFormat[0];
currentCaptureHeight = captureFormat[1];
videoCapturer.startCapture(currentCaptureWidth, currentCaptureHeight, TARGET_FPS);
videoSource.adaptOutputFormat(TARGET_WIDTH, TARGET_HEIGHT, TARGET_FPS);
isCapturing = true;
Log.d(
TAG,
"Video capture started at "
+ currentCaptureWidth
+ "x"
+ currentCaptureHeight
+ ", adapted to "
+ TARGET_WIDTH
+ "x"
+ TARGET_HEIGHT);
return true;
}
/** Stop the camera. The capturer is kept alive. */
public synchronized void stopVideoCapture() {
if (!isCapturing) {
return;
}
if (videoCapturer != null) {
try {
videoCapturer.stopCapture();
Log.d(TAG, "Video capture stopped");
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while stopping capture", e);
Thread.currentThread().interrupt();
}
}
isCapturing = false;
}
private int[] selectCaptureFormat(@Nullable String deviceName) {
if (deviceName == null) {
Log.w(TAG, "Device name is null, using target dimensions");
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
}
Camera2Enumerator enumerator = new Camera2Enumerator(context);
List<CameraEnumerationAndroid.CaptureFormat> formats =
enumerator.getSupportedFormats(deviceName);
if (formats == null || formats.isEmpty()) {
Log.w(TAG, "No supported formats for " + deviceName);
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
}
CameraEnumerationAndroid.CaptureFormat best = null;
int bestPixels = Integer.MAX_VALUE;
for (CameraEnumerationAndroid.CaptureFormat f : formats) {
if (f.width >= TARGET_WIDTH && f.height >= TARGET_HEIGHT) {
int pixels = f.width * f.height;
if (pixels < bestPixels) {
bestPixels = pixels;
best = f;
}
}
}
if (best != null) {
Log.d(
TAG, "Selected capture format: " + best.width + "x" + best.height + " for " + deviceName);
return new int[] {best.width, best.height};
}
CameraEnumerationAndroid.CaptureFormat largest = null;
int largestPixels = 0;
for (CameraEnumerationAndroid.CaptureFormat f : formats) {
int pixels = f.width * f.height;
if (pixels > largestPixels) {
largestPixels = pixels;
largest = f;
}
}
if (largest != null) {
Log.w(
TAG,
"Using largest format " + largest.width + "x" + largest.height + " for " + deviceName);
return new int[] {largest.width, largest.height};
}
return new int[] {TARGET_WIDTH, TARGET_HEIGHT};
}
@Nullable
private VideoCapturer createVideoCapturer() {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
@@ -111,6 +231,7 @@ public class MediaStreamManager {
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer != null) {
isFrontCamera = true;
currentDeviceName = deviceName;
return capturer;
}
}
@@ -121,6 +242,7 @@ public class MediaStreamManager {
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer != null) {
isFrontCamera = enumerator.isFrontFacing(deviceName);
currentDeviceName = deviceName;
return capturer;
}
}
@@ -129,6 +251,14 @@ public class MediaStreamManager {
}
public void switchCamera(@Nullable CameraSwitchCallback callback) {
if (!isCapturing) {
Log.w(TAG, "Cannot switch camera while not capturing");
if (callback != null) {
callback.onError("Camera not active");
}
return;
}
if (!(videoCapturer instanceof CameraVideoCapturer)) {
Log.e(TAG, "switchCamera called but videoCapturer is not a CameraVideoCapturer");
return;
@@ -169,6 +299,30 @@ public class MediaStreamManager {
public void onCameraSwitchDone(boolean isFront) {
Log.d(TAG, "switchCamera SUCCESS, isFront=" + isFront);
isFrontCamera = isFront;
currentDeviceName = finalTargetCameraName;
int[] newFormat = selectCaptureFormat(finalTargetCameraName);
if (newFormat[0] != currentCaptureWidth || newFormat[1] != currentCaptureHeight) {
Log.d(
TAG,
"Changing capture format: "
+ currentCaptureWidth
+ "x"
+ currentCaptureHeight
+ " to "
+ newFormat[0]
+ "x"
+ newFormat[1]);
currentCaptureWidth = newFormat[0];
currentCaptureHeight = newFormat[1];
cameraVideoCapturer.changeCaptureFormat(
currentCaptureWidth, currentCaptureHeight, TARGET_FPS);
}
if (videoSource != null) {
videoSource.adaptOutputFormat(TARGET_WIDTH, TARGET_HEIGHT, TARGET_FPS);
}
if (callback != null) callback.onCameraSwitch(isFront);
}
@@ -186,10 +340,12 @@ public class MediaStreamManager {
}
/** Cleanup resources */
public void dispose() {
public synchronized void dispose() {
if (videoCapturer != null) {
try {
videoCapturer.stopCapture();
if (isCapturing) {
videoCapturer.stopCapture();
}
} catch (InterruptedException e) {
Log.e(TAG, "Error stopping capture", e);
}
@@ -197,6 +353,8 @@ public class MediaStreamManager {
videoCapturer = null;
}
isCapturing = false;
if (surfaceTextureHelper != null) {
surfaceTextureHelper.dispose();
surfaceTextureHelper = null;
@@ -1,31 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;
import org.thoughtcrime.securesms.components.viewpager.HackyViewPager;
/** An implementation of {@link ViewPager} that disables swiping when the view is disabled. */
public class ControllableViewPager extends HackyViewPager {
public ControllableViewPager(@NonNull Context context) {
super(context);
}
public ControllableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return isEnabled() && super.onTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isEnabled() && super.onInterceptTouchEvent(ev);
}
}
@@ -360,6 +360,31 @@ public class InputPanel extends ConstraintLayout
}
}
public boolean isRecording() {
return microphoneRecorderView.isRecording();
}
public long getRecordingDuration() {
return recordTime.getElapsedTime();
}
public void resetRecordingUI() {
microphoneRecorderView.resetState();
recordLockCancel.setVisibility(View.GONE);
recordTime.hide();
slideToCancel.hide();
emojiToggle.setVisibility(View.VISIBLE);
emojiToggle.setAlpha(1f);
composeText.setVisibility(View.VISIBLE);
composeText.setAlpha(1f);
quickCameraToggle.setVisibility(View.VISIBLE);
quickCameraToggle.setAlpha(1f);
quickAudioToggle.setVisibility(View.VISIBLE);
quickAudioToggle.setAlpha(1f);
buttonToggle.setAlpha(1f);
}
public interface Listener {
void onRecorderStarted();
@@ -452,10 +477,10 @@ public class InputPanel extends ConstraintLayout
}
public long hide() {
long elapsedtime = System.currentTimeMillis() - startTime.get();
long elapsedTime = getElapsedTime();
this.startTime.set(0);
ViewUtil.fadeOut(this.recordTimeView, FADE_TIME, View.INVISIBLE);
return elapsedtime;
return elapsedTime;
}
@Override
@@ -468,6 +493,11 @@ public class InputPanel extends ConstraintLayout
}
}
public long getElapsedTime() {
long start = startTime.get();
return start > 0 ? System.currentTimeMillis() - start : 0;
}
private String formatElapsedTime(long ms) {
return DateUtils.formatElapsedTime(TimeUnit.MILLISECONDS.toSeconds(ms))
+ String.format(".%01d", ((ms / 100) % 10));
@@ -53,10 +53,11 @@ public class MediaView extends FrameLayout {
@NonNull Window window,
@NonNull Uri source,
@Nullable String fileName,
@NonNull String mediaType,
@Nullable String mediaType,
long size,
boolean autoplay)
throws IOException {
mediaType = mediaType == null ? "null" : mediaType;
if (mediaType.startsWith("image/")) {
imageView.setVisibility(View.VISIBLE);
if (videoView.resolved()) videoView.get().setVisibility(View.GONE);
@@ -287,4 +287,15 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
.start();
}
}
public boolean isRecording() {
return state != State.NOT_RUNNING;
}
public void resetState() {
if (state != State.NOT_RUNNING) {
state = State.NOT_RUNNING;
hideUi();
}
}
}
@@ -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);
}
}
@@ -1,22 +0,0 @@
package org.thoughtcrime.securesms.components.viewpager;
import androidx.viewpager.widget.ViewPager;
public abstract class ExtendedOnPageChangedListener implements ViewPager.OnPageChangeListener {
private Integer currentPage = null;
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
if (currentPage != null && currentPage != position) onPageUnselected(currentPage);
currentPage = position;
}
public abstract void onPageUnselected(int position);
@Override
public void onPageScrollStateChanged(int state) {}
}
@@ -1,41 +0,0 @@
package org.thoughtcrime.securesms.components.viewpager;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import androidx.viewpager.widget.ViewPager;
/**
* Hacky fix for http://code.google.com/p/android/issues/detail?id=18990
*
* <p>ScaleGestureDetector seems to mess up the touch events, which means that ViewGroups which make
* use of onInterceptTouchEvent throw a lot of IllegalArgumentException: pointerIndex out of range.
*
* <p>There's not much I can do in my code for now, but we can mask the result by just catching the
* problem and ignoring it.
*
* @author Chris Banes
*/
public class HackyViewPager extends ViewPager {
private static final String TAG = "HackyViewPager";
public HackyViewPager(Context context) {
super(context);
}
public HackyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
Log.w(TAG, e);
return false;
}
}
}
@@ -46,7 +46,7 @@ public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
if (query == null && addScanQRLink) {
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_QR_INVITE);
}
if (addCreateContactLink && !dcContext.isChatmail()) {
if (addCreateContactLink && dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
additional_items =
Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT);
}
@@ -54,7 +54,7 @@ public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_GROUP);
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST);
if (!dcContext.isChatmail()) {
if (dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) {
additional_items =
Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP);
}
@@ -23,6 +23,10 @@ import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import com.b44t.messenger.DcMsg;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.HashMap;
import org.thoughtcrime.securesms.ApplicationContext;
@@ -31,6 +35,7 @@ import org.thoughtcrime.securesms.LocalHelpActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.notifications.NotificationCenter;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.qr.QrActivity;
@@ -48,16 +53,14 @@ public class DcHelper {
public static final String CONFIG_DISPLAY_NAME = "displayname";
public static final String CONFIG_SELF_STATUS = "selfstatus";
public static final String CONFIG_SELF_AVATAR = "selfavatar";
public static final String CONFIG_MVBOX_MOVE = "mvbox_move";
public static final String CONFIG_ONLY_FETCH_MVBOX = "only_fetch_mvbox";
public static final String CONFIG_BCC_SELF = "bcc_self";
public static final String CONFIG_SHOW_EMAILS = "show_emails";
public static final String CONFIG_MEDIA_QUALITY = "media_quality";
public static final String CONFIG_PROXY_ENABLED = "proxy_enabled";
public static final String CONFIG_PROXY_URL = "proxy_url";
public static final String CONFIG_PRIVATE_TAG = "private_tag";
public static final String CONFIG_STATS_SENDING = "stats_sending";
public static final String CONFIG_STATS_ID = "stats_id";
public static final String CONFIG_FORCE_ENCRYPTION = "force_encryption";
public static DcContext getContext(@NonNull Context context) {
return ApplicationContext.getInstance(context).getDcContext();
@@ -443,6 +446,15 @@ public class DcHelper {
return getBlobdirFile(dcContext, filename, ext);
}
public static String copyToBlobdir(Context context, Uri uri, String filename, String ext)
throws IOException {
String path = getBlobdirFile(getContext(context), filename, ext);
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
OutputStream outputStream = new FileOutputStream(path);
Util.copy(inputStream, outputStream);
return path;
}
@NonNull
public static ThreadRecord getThreadRecord(
Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
@@ -516,16 +528,20 @@ public class DcHelper {
.show();
}
public static void showInvalidUnencryptedDialog(Context context) {
new AlertDialog.Builder(context)
public static AlertDialog.Builder prepareInvalidUnencryptedDialog(
Context context, AlertDialog.Builder builder) {
return builder
.setMessage(context.getString(R.string.invalid_unencrypted_explanation))
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#howtoe2ee"))
.setNegativeButton(
R.string.qrscan_title,
(d, w) -> context.startActivity(new Intent(context, QrActivity.class)))
.setPositiveButton(R.string.ok, null)
.setCancelable(true)
.show();
.setCancelable(true);
}
public static void showInvalidUnencryptedDialog(Context context) {
prepareInvalidUnencryptedDialog(context, new AlertDialog.Builder(context)).show();
}
public static void openHelp(Context context, String section) {
@@ -1,53 +0,0 @@
package org.thoughtcrime.securesms.geolocation;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.HashSet;
import java.util.Set;
public final class ActiveLocationChats {
private static final String PREFS_NAME = "location_streaming";
private static final String KEY_ACTIVE = "active_chat_ids";
private ActiveLocationChats() {}
private static SharedPreferences prefs(Context context) {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
/**
* Add a chat. Uses commit() to guarantee the write reaches disk before the process can die to
* preserve the superset invariant.
*/
static void add(Context context, int chatId) {
Set<String> current = new HashSet<>(getAll(context));
current.add(String.valueOf(chatId));
prefs(context).edit().putStringSet(KEY_ACTIVE, current).commit();
}
public static void remove(Context context, int chatId) {
Set<String> current = new HashSet<>(getAll(context));
current.remove(String.valueOf(chatId));
prefs(context).edit().putStringSet(KEY_ACTIVE, current).apply();
}
static void clear(Context context) {
prefs(context).edit().remove(KEY_ACTIVE).apply();
}
static Set<Integer> getAllIds(Context context) {
Set<Integer> ids = new HashSet<>();
for (String s : getAll(context)) {
try {
ids.add(Integer.parseInt(s));
} catch (NumberFormatException ignored) {
}
}
return ids;
}
private static Set<String> getAll(Context context) {
return prefs(context).getStringSet(KEY_ACTIVE, new HashSet<>());
}
}
@@ -17,13 +17,15 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.content.ContextCompat;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
public class LocationStreamingService extends Service {
private static final String TAG = "LocationStreamingService";
private static final String TAG = "LocationStreamingSvc";
private static final String ACTION_STOP = "org.thoughtcrime.securesms.geolocation.STOP_STREAMING";
private static final int NOTIFICATION_ID = 8801;
private static final String CHANNEL_ID = "location_streaming";
@@ -36,38 +38,63 @@ public class LocationStreamingService extends Service {
// static API
/** Register a chat for location updates, then ensure the service is running. */
public static void startSharing(Context context, int chatId, int durationSeconds) {
ActiveLocationChats.add(context, chatId);
DcHelper.getContext(context).sendLocationsToChat(chatId, durationSeconds);
ContextCompat.startForegroundService(
context, new Intent(context, LocationStreamingService.class));
}
/** Unregister a chat. If no chats remain, stop the service. */
public static void stopSharing(Context context, int chatId) {
ActiveLocationChats.remove(context, chatId);
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
if (!DcHelper.getContext(context).isSendingLocationsToChat(0)) {
context.stopService(new Intent(context, LocationStreamingService.class));
}
}
public static void ensureRunning(Context context) {
if (!hasLocationPermission(context)) {
for (int chatId : ActiveLocationChats.getAllIds(context)) {
DcHelper.getContext(context).sendLocationsToChat(chatId, 0);
}
ActiveLocationChats.clear(context);
public static void startSharing(Context context, int accountId, int chatId, int durationSeconds) {
try {
DcHelper.getRpc(context).sendLocationsToChat(accountId, chatId, durationSeconds);
} catch (RpcException e) {
Log.e(TAG, "Failed to start location streaming", e);
return;
}
ContextCompat.startForegroundService(
context, new Intent(context, LocationStreamingService.class));
}
/** Unregister a chat. If no chats remain, stop the service. */
public static void stopSharing(Context context, int accountId, int chatId) {
try {
DcHelper.getRpc(context).sendLocationsToChat(accountId, chatId, 0);
} catch (RpcException e) {
Log.e(TAG, "Failed to stop location streaming", e);
}
if (!isAnySharingActive(context)) {
context.stopService(new Intent(context, LocationStreamingService.class));
}
}
public static void ensureRunning(Context context) {
if (!hasLocationPermission(context)) {
try {
DcHelper.getRpc(context).stopSendingLocations();
} catch (RpcException e) {
Log.e(TAG, "Failed to stop location streaming", e);
}
return;
}
if (isAnySharingActive(context)) {
ContextCompat.startForegroundService(
context, new Intent(context, LocationStreamingService.class));
}
}
public static boolean isRunning() {
return running;
}
private static boolean isAnySharingActive(Context context) {
try {
Rpc rpc = DcHelper.getRpc(context);
for (int accountId : rpc.getAllAccountIds()) {
if (rpc.isSendingLocations(accountId)) {
return true;
}
}
} catch (RpcException e) {
Log.e(TAG, "Failed to check location streaming state", e);
return true;
}
return false;
}
// lifecycle
@Override
@@ -86,7 +113,7 @@ public class LocationStreamingService extends Service {
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
stopAllSharing();
stopAllSharing(this);
stopSelf();
return START_NOT_STICKY;
}
@@ -100,11 +127,12 @@ public class LocationStreamingService extends Service {
return START_STICKY;
}
private void stopAllSharing() {
for (int chatId : ActiveLocationChats.getAllIds(this)) {
DcHelper.getContext(this).sendLocationsToChat(chatId, 0);
private static void stopAllSharing(Context context) {
try {
DcHelper.getRpc(context).stopSendingLocations();
} catch (RpcException e) {
Log.e(TAG, "Failed to stop sending locations", e);
}
ActiveLocationChats.clear(this);
}
@Nullable
@@ -150,16 +178,21 @@ public class LocationStreamingService extends Service {
private void publishAndWrite(Location location) {
LocationData.getInstance().post(location);
boolean keepGoing =
DcHelper.getContext(this)
.setLocation(
(float) location.getLatitude(),
(float) location.getLongitude(),
location.getAccuracy());
boolean keepGoing;
try {
keepGoing =
DcHelper.getRpc(this)
.setLocation(
(float) location.getLatitude(),
(float) location.getLongitude(),
location.getAccuracy());
} catch (RpcException e) {
Log.e(TAG, "Failed to set location", e);
return;
}
Log.d(TAG, "keepGoing: " + keepGoing);
if (!keepGoing) {
stopAllSharing();
stopSelf();
}
}
@@ -188,9 +221,8 @@ public class LocationStreamingService extends Service {
NotificationChannel channel =
new NotificationChannel(
CHANNEL_ID,
getString(R.string.location_streaming_notification_title),
getString(R.string.pref_on_demand_location_streaming),
NotificationManager.IMPORTANCE_LOW);
channel.setDescription(getString(R.string.location_streaming_channel_desc));
channel.setShowBadge(false);
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) nm.createNotificationChannel(channel);
@@ -209,7 +241,7 @@ public class LocationStreamingService extends Service {
PendingIntent.getService(this, 1, stopIntent, PendingIntent.FLAG_IMMUTABLE);
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.location_streaming_notification_title))
.setContentTitle(getString(R.string.pref_on_demand_location_streaming))
.setContentText(getString(R.string.location_streaming_notification_text))
.setSmallIcon(R.drawable.ic_location_on_white_24dp)
.setOngoing(true)
@@ -17,14 +17,12 @@
package org.thoughtcrime.securesms.mms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
@@ -36,6 +34,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import chat.delta.util.ListenableFuture;
import chat.delta.util.SettableFuture;
@@ -63,22 +62,21 @@ import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.geolocation.ActiveLocationChats;
import org.thoughtcrime.securesms.geolocation.LocationStreamingService;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.guava.Optional;
import org.thoughtcrime.securesms.util.views.Stub;
public class AttachmentManager {
private static final String TAG = "AttachmentManager";
private final @NonNull Context context;
private final @NonNull Stub<View> attachmentViewStub;
private final @NonNull View attachmentView;
private final @NonNull AttachmentListener attachmentListener;
private RemovableEditableMediaView removableMediaView;
@@ -99,20 +97,16 @@ public class AttachmentManager {
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
this.context = activity;
this.attachmentListener = listener;
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
}
private void inflateStub() {
if (!attachmentViewStub.resolved()) {
View root = attachmentViewStub.get();
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc);
this.vcardView = ViewUtil.findById(root, R.id.attachment_vcard);
// this.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
this.attachmentView = ViewUtil.findById(activity, R.id.attachment_editor);
if (this.attachmentView != null) {
this.thumbnail = ViewUtil.findById(attachmentView, R.id.attachment_thumbnail);
this.audioView = ViewUtil.findById(attachmentView, R.id.attachment_audio);
this.documentView = ViewUtil.findById(attachmentView, R.id.attachment_document);
this.webxdcView = ViewUtil.findById(attachmentView, R.id.attachment_webxdc);
this.vcardView = ViewUtil.findById(attachmentView, R.id.attachment_vcard);
// this.mapView = ViewUtil.findById(attachmentView, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(attachmentView, R.id.removable_media_view);
removableMediaView.addRemoveClickListener(new RemoveButtonListener());
removableMediaView.setEditClickListener(new EditButtonListener());
@@ -121,10 +115,10 @@ public class AttachmentManager {
}
public void clear(@NonNull GlideRequests glideRequests, boolean animate) {
if (attachmentViewStub.resolved()) {
if (this.attachmentView != null) {
if (animate) {
ViewUtil.fadeOut(attachmentViewStub.get(), 200)
ViewUtil.fadeOut(attachmentView, 200)
.addListener(
new ListenableFuture.Listener<Boolean>() {
@Override
@@ -221,7 +215,6 @@ public class AttachmentManager {
}
*/
@SuppressLint("StaticFieldLeak")
public ListenableFuture<Boolean> setMedia(
@NonNull final GlideRequests glideRequests,
@NonNull final Uri uri,
@@ -231,149 +224,104 @@ public class AttachmentManager {
final int height,
final int chatId,
AudioPlaybackViewModel playbackViewModel) {
inflateStub();
final SettableFuture<Boolean> result = new SettableFuture<>();
new AsyncTask<Void, Void, Slide>() {
@Override
protected void onPreExecute() {
thumbnail.clear(glideRequests);
setAttachmentPresent(true);
}
thumbnail.clear(glideRequests);
setAttachmentPresent(true);
@Override
protected @Nullable Slide doInBackground(Void... params) {
try {
if (msg != null && msg.getType() == DcMsg.DC_MSG_WEBXDC) {
return new DocumentSlide(context, msg);
} else if (PartAuthority.isLocalUri(uri)) {
return getManuallyCalculatedSlideInfo(uri, width, height, msg);
} else {
Slide result = getContentResolverSlideInfo(uri, width, height, chatId);
if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height, msg);
else return result;
}
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(@Nullable final Slide slide) {
if (slide == null) {
setAttachmentPresent(false);
result.set(false);
} else if (slide.getFileSize() > 1024 * 1024 * 1024) {
// this is only a rough check, videos and images may be recoded
// and the core checks more carefully later.
setAttachmentPresent(false);
Log.w(TAG, "File too large.");
Toast.makeText(slide.context, "File too large.", Toast.LENGTH_LONG).show();
result.set(false);
} else {
setSlide(slide);
setAttachmentPresent(true);
if (slide.hasAudio()) {
audioView.setPlaybackViewModel(playbackViewModel);
audioView.setAudio((AudioSlide) slide);
removableMediaView.display(audioView, false);
removableMediaView.addRemoveClickListener(
v -> {
playbackViewModel.stop(audioView.getMsgId());
});
result.set(true);
} else if (slide.isVcard()) {
vcardView.setVcard(glideRequests, (VcardSlide) slide, DcHelper.getRpc(context));
removableMediaView.display(vcardView, false);
} else if (slide.hasDocument()) {
if (slide.isWebxdcDocument()) {
DcMsg instance =
msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId);
webxdcView.setWebxdc(instance, context.getString(R.string.webxdc_draft_hint));
webxdcView.setWebxdcClickListener(
(v, s) -> {
WebxdcActivity.openWebxdcActivity(context, instance);
});
removableMediaView.display(webxdcView, false);
Util.runOnBackground(
() -> {
Slide slide = null;
try {
if (msg != null && msg.getType() == DcMsg.DC_MSG_WEBXDC) {
slide = new DocumentSlide(context, msg);
} else if (msg != null
&& (msg.getType() == DcMsg.DC_MSG_AUDIO || msg.getType() == DcMsg.DC_MSG_VOICE)) {
slide = new AudioSlide(context, msg);
} else if (PartAuthority.isLocalUri(uri)) {
slide = getManuallyCalculatedSlideInfo(uri, width, height, msg, mediaType, chatId);
} else {
documentView.setDocument((DocumentSlide) slide);
removableMediaView.display(documentView, false);
slide = getContentResolverSlideInfo(uri, width, height, chatId, mediaType);
if (slide == null) {
slide = getManuallyCalculatedSlideInfo(uri, width, height, msg, mediaType, chatId);
}
}
result.set(true);
} else {
Attachment attachment = slide.asAttachment();
result.deferTo(
thumbnail.setImageResource(
glideRequests, slide, attachment.getWidth(), attachment.getHeight()));
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
} catch (IOException e) {
Log.w(TAG, e);
}
attachmentListener.onAttachmentChanged();
}
}
final Slide finalSlide = slide;
Util.runOnMain(
() -> {
if (finalSlide == null) {
setAttachmentPresent(false);
result.set(false);
} else if (finalSlide.getFileSize() > 1024 * 1024 * 1024) {
// this is only a rough check, videos and images may be recoded
// and the core checks more carefully later.
setAttachmentPresent(false);
Log.w(TAG, "File too large.");
Toast.makeText(finalSlide.context, "File too large.", Toast.LENGTH_LONG).show();
result.set(false);
} else {
setSlide(finalSlide);
setAttachmentPresent(true);
private @Nullable Slide getContentResolverSlideInfo(
Uri uri, int width, int height, int chatId) {
if (finalSlide.hasAudio()) {
audioView.setPlaybackViewModel(playbackViewModel);
audioView.setAudio((AudioSlide) finalSlide);
removableMediaView.display(audioView, false);
removableMediaView.addRemoveClickListener(
v -> playbackViewModel.stop(audioView.getMsgId()));
result.set(true);
} else if (finalSlide.isVcard()) {
vcardView.setVcard(
glideRequests, (VcardSlide) finalSlide, DcHelper.getRpc(context));
removableMediaView.display(vcardView, false);
} else if (finalSlide.hasDocument()) {
if (finalSlide.isWebxdcDocument()) {
DcMsg instance =
msg != null
? msg
: DcHelper.getContext(context).getMsg(finalSlide.dcMsgId);
webxdcView.setWebxdc(instance, context.getString(R.string.webxdc_draft_hint));
webxdcView.setWebxdcClickListener(
(v, s) -> WebxdcActivity.openWebxdcActivity(context, instance));
removableMediaView.display(webxdcView, false);
} else {
documentView.setDocument((DocumentSlide) finalSlide);
removableMediaView.display(documentView, false);
}
result.set(true);
} else {
Attachment attachment = finalSlide.asAttachment();
result.deferTo(
thumbnail.setImageResource(
glideRequests,
finalSlide,
attachment.getWidth(),
attachment.getHeight()));
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
}
long start = System.currentTimeMillis();
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
attachmentListener.onAttachmentChanged();
}
});
});
if (cursor != null && cursor.moveToFirst()) {
String fileName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
String mimeType = context.getContentResolver().getType(uri);
return result;
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
width = dimens.first;
height = dimens.second;
}
Log.w(
TAG,
"remote slide with size "
+ fileSize
+ " took "
+ (System.currentTimeMillis() - start)
+ "ms");
return mediaType.createSlide(
context, uri, fileName, mimeType, fileSize, width, height, chatId);
}
}
return null;
}
private @NonNull Slide getManuallyCalculatedSlideInfo(
Uri uri, int width, int height, @Nullable DcMsg msg) throws IOException {
long start = System.currentTimeMillis();
Long mediaSize = null;
String fileName = null;
String mimeType = null;
if (msg != null) {
fileName = msg.getFilename();
mimeType = msg.getFilemime();
}
if (PartAuthority.isLocalUri(uri)) {
mediaSize = PartAuthority.getAttachmentSize(context, uri);
if (fileName == null) fileName = PartAuthority.getAttachmentFileName(context, uri);
if (mimeType == null) mimeType = PartAuthority.getAttachmentContentType(context, uri);
}
if (mediaSize == null) {
mediaSize = MediaUtil.getMediaSize(context, uri);
}
if (mimeType == null) {
mimeType = MediaUtil.getMimeType(context, uri);
}
private @Nullable Slide getContentResolverSlideInfo(
Uri uri, int width, int height, int chatId, MediaType mediaType) {
long start = System.currentTimeMillis();
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
String fileName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
long mediaSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
String mimeType = context.getContentResolver().getType(uri);
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
@@ -381,14 +329,6 @@ public class AttachmentManager {
height = dimens.second;
}
if (fileName == null) {
try {
fileName = new File(uri.getPath()).getName();
} catch (Exception e) {
Log.w(TAG, "Could not get file name from uri: " + e);
}
}
Log.w(
TAG,
"local slide with size "
@@ -399,9 +339,61 @@ public class AttachmentManager {
return mediaType.createSlide(
context, uri, fileName, mimeType, mediaSize, width, height, chatId);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
return result;
return null;
}
private @NonNull Slide getManuallyCalculatedSlideInfo(
Uri uri, int width, int height, @Nullable DcMsg msg, MediaType mediaType, int chatId)
throws IOException {
long start = System.currentTimeMillis();
Long mediaSize = null;
String fileName = null;
String mimeType = null;
if (msg != null) {
fileName = msg.getFilename();
mimeType = msg.getFilemime();
}
if (PartAuthority.isLocalUri(uri)) {
mediaSize = PartAuthority.getAttachmentSize(context, uri);
if (fileName == null) fileName = PartAuthority.getAttachmentFileName(context, uri);
if (mimeType == null) mimeType = PartAuthority.getAttachmentContentType(context, uri);
}
if (mediaSize == null) {
mediaSize = MediaUtil.getMediaSize(context, uri);
}
if (mimeType == null) {
mimeType = MediaUtil.getMimeType(context, uri);
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
width = dimens.first;
height = dimens.second;
}
if (fileName == null) {
try {
fileName = new File(uri.getPath()).getName();
} catch (Exception e) {
Log.w(TAG, "Could not get file name from uri: " + e);
}
}
Log.w(
TAG,
"local slide with size "
+ mediaSize
+ " took "
+ (System.currentTimeMillis() - start)
+ "ms");
return mediaType.createSlide(
context, uri, fileName, mimeType, mediaSize, width, height, chatId);
}
// should be called when the attachment manager comes into view again.
@@ -447,7 +439,7 @@ public class AttachmentManager {
}
public static void selectDocument(Activity activity, int requestCode) {
selectMediaType(activity, "*/*", null, requestCode);
selectMediaType(activity, "*/*", null, requestCode, null, true);
}
public static void selectWebxdc(Activity activity, int requestCode) {
@@ -485,22 +477,35 @@ public class AttachmentManager {
.ifNecessary()
.withPermanentDenialDialog(
activity.getString(R.string.perm_explain_access_to_storage_denied))
.onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode))
.onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode, null, false))
.execute();
}
public static void selectLocation(Activity activity, int chatId) {
Context appContext = activity.getApplicationContext();
Rpc rpc = DcHelper.getRpc(appContext);
int accountId = DcHelper.getContext(appContext).getAccountId();
if (DcHelper.getContext(appContext).isSendingLocationsToChat(chatId)) {
boolean currentlySharing;
try {
currentlySharing = rpc.isSendingLocationsToChat(accountId, chatId);
} catch (RpcException e) {
Log.e(TAG, "Failed to check location streaming state", e);
return;
}
if (currentlySharing) {
if (LocationStreamingService.isRunning()) {
LocationStreamingService.stopSharing(appContext, chatId);
LocationStreamingService.stopSharing(appContext, accountId, chatId);
return;
}
// Stale service is dead but chat layer still thinks it's sharing.
// Clean up this chat and fall through to the fresh start flow.
ActiveLocationChats.remove(appContext, chatId);
DcHelper.getContext(appContext).sendLocationsToChat(chatId, 0);
// Clean up and fall through to the fresh start flow.
try {
rpc.sendLocationsToChat(accountId, chatId, 0);
} catch (RpcException e) {
Log.e(TAG, "Failed to stop stale location streaming", e);
}
}
Permissions.with(activity)
@@ -514,7 +519,8 @@ public class AttachmentManager {
ShareLocationDialog.show(
activity,
durationInSeconds ->
LocationStreamingService.startSharing(appContext, chatId, durationInSeconds));
LocationStreamingService.startSharing(
appContext, accountId, chatId, durationInSeconds));
})
.request(
android.Manifest.permission.ACCESS_FINE_LOCATION,
@@ -596,20 +602,6 @@ public class AttachmentManager {
.execute();
}
public static void selectMediaType(
Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
selectMediaType(activity, type, extraMimeType, requestCode, null, false);
}
public static void selectMediaType(
Activity activity,
@NonNull String type,
@Nullable String[] extraMimeType,
int requestCode,
@Nullable Uri initialUri) {
selectMediaType(activity, type, extraMimeType, requestCode, initialUri, false);
}
public static void selectMediaType(
Activity activity,
@NonNull String type,
@@ -713,13 +705,31 @@ public class AttachmentManager {
mimeType = "application/octet-stream";
}
DcContext dcContext = DcHelper.getContext(context);
switch (this) {
case IMAGE:
return new ImageSlide(context, uri, fileName, dataSize, width, height);
case GIF:
return new GifSlide(context, uri, fileName, dataSize, width, height);
case AUDIO:
return new AudioSlide(context, uri, dataSize, false, fileName);
DcMsg audioMsg = new DcMsg(dcContext, DcMsg.DC_MSG_AUDIO);
Attachment audioAttachment =
new UriAttachment(
uri,
null,
mimeType,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
dataSize,
0,
0,
fileName,
null,
false);
String audioPath = audioAttachment.getRealPath(context);
audioMsg.setFileAndDeduplicate(audioPath, fileName, mimeType);
dcContext.setDraft(chatId, audioMsg);
return new AudioSlide(context, audioMsg);
case VIDEO:
return new VideoSlide(context, uri, fileName, dataSize);
case DOCUMENT:
@@ -727,7 +737,6 @@ public class AttachmentManager {
// draft
// is set. Therefore we need to create a DcMsg already now.
if (fileName != null && fileName.endsWith(".xdc")) {
DcContext dcContext = DcHelper.getContext(context);
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC);
Attachment attachment =
new UriAttachment(
@@ -772,6 +781,10 @@ public class AttachmentManager {
updateVisibility();
}
public boolean isHidden() {
return hidden;
}
private void setAttachmentPresent(boolean isPresent) {
this.attachmentPresent = isPresent;
updateVisibility();
@@ -784,9 +797,9 @@ public class AttachmentManager {
} else {
vis = View.GONE;
}
if (vis == View.GONE && !attachmentViewStub.resolved()) {
if (attachmentView == null) {
return;
}
attachmentViewStub.get().setVisibility(vis);
attachmentView.setVisibility(vis);
}
}
@@ -78,4 +78,13 @@ public class SlideDeck {
}
return 0;
}
public int getAudioDraftId() {
for (Slide slide : slides) {
if (slide.hasAudio() && slide.dcMsgId != 0) {
return slide.dcMsgId;
}
}
return 0;
}
}
@@ -11,6 +11,7 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioAttributes;
import android.media.RingtoneManager;
@@ -23,13 +24,18 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.HashMap;
@@ -42,6 +48,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
@@ -61,8 +68,18 @@ public class NotificationCenter {
private volatile long lastAudibleNotification = 0;
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
// Map<accountId, Map<chatId, lines>, contains the last lines of each chat for each account
private final HashMap<Integer, HashMap<Integer, LinkedHashMap<Integer, String>>> inboxes =
private static class NotifData {
final Person sender;
final String text;
NotifData(Person sender, String text) {
this.sender = sender;
this.text = text;
}
}
// notification history of each chat for each account
private final HashMap<Integer, HashMap<Integer, LinkedHashMap<Integer, NotifData>>> inboxes =
new HashMap<>();
public NotificationCenter(Context context) {
@@ -410,31 +427,42 @@ public class NotificationCenter {
DcMsg dcMsg = dcContext.getMsg(msgId);
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
String shortLine =
DcContact sender = dcContext.getContact(dcMsg.getFromId());
String senderName = dcMsg.getSenderName(sender);
String personId = accountId + "-" + dcMsg.getFromId();
if (dcMsg.getOverrideSenderName() != null) {
// we need to treat the contact as a separate Person with different ID
// otherwise the name will be overwritten by future notifications
personId += "-" + senderName;
}
String text =
privacy.isDisplayMessage()
? dcMsg.getSummarytext(2000)
: context.getString(R.string.notify_new_message);
String shortLine = text;
if (dcChat.isMultiUser() && privacy.isDisplayContact()) {
shortLine =
dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + shortLine;
shortLine = senderName + ": " + text;
}
String tickerLine = shortLine;
if (!dcChat.isMultiUser() && privacy.isDisplayContact()) {
tickerLine =
dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + tickerLine;
if (dcMsg.getOverrideSenderName() != null) {
// There is an "overridden" display name on the message, so, we need to prepend the
// display name to the message,
// i.e. set the shortLine to be the same as the tickerLine.
shortLine = tickerLine;
}
NotifData notifData =
new NotifData(
new Person.Builder()
.setName(senderName)
.setIcon(getAvatarIcon(sender))
.setBot(sender.isBot())
.setKey(personId)
.build(),
text);
String tickerLine = text;
if (privacy.isDisplayContact()) {
tickerLine = senderName + ": " + text;
}
DcMsg quotedMsg = dcMsg.getQuotedMsg();
boolean isMention = dcChat.isMultiUser() && quotedMsg != null && quotedMsg.isOutgoing();
maybeAddNotification(accountId, dcChat, msgId, shortLine, tickerLine, true, isMention);
maybeAddNotification(accountId, dcChat, msgId, notifData, tickerLine, true, isMention);
});
}
@@ -450,16 +478,27 @@ public class NotificationCenter {
// just do nothing.
}
DcContact sender = dcContext.getContact(contactId);
String shortLine =
DcContact contact = dcContext.getContact(contactId);
String text =
context.getString(
R.string.reaction_by_other,
sender.getDisplayName(),
contact.getDisplayName(),
reaction,
dcMsg.getSummarytext(2000));
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
NotifData notifData =
new NotifData(
new Person.Builder()
.setName(contact.getDisplayName())
.setIcon(getAvatarIcon(contact))
.setBot(contact.isBot())
.setKey(accountId + "-" + contactId)
.build(),
text);
maybeAddNotification(
accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser());
accountId, dcChat, msgId, notifData, text, false, dcChat.isMultiUser());
});
}
@@ -473,6 +512,7 @@ public class NotificationCenter {
DcContext dcContext = context.getDcAccounts().getAccount(accountId);
DcMsg dcMsg = dcContext.getMsg(msgId);
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
DcMsg parentMsg;
if (dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) {
parentMsg = dcMsg;
@@ -486,10 +526,34 @@ public class NotificationCenter {
JSONObject info = parentMsg.getWebxdcInfo();
final String name = JsonUtils.optString(info, "name");
String shortLine = name.isEmpty() ? text : (name + ": " + text);
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
String tickerLine = name.isEmpty() ? text : (name + ": " + text);
NotifData notifData;
if (dcChat.isMultiUser()) {
byte[] blob = parentMsg.getWebxdcBlob(JsonUtils.optString(info, "icon"));
notifData =
new NotifData(
new Person.Builder()
.setName(name)
.setIcon(getAvatarIcon(blob))
.setKey(accountId + "-webxdc-" + msgId)
.build(),
text);
} else {
DcContact sender = dcContext.getContact(contactId);
notifData =
new NotifData(
new Person.Builder()
.setName(sender.getDisplayName())
.setIcon(getAvatarIcon(sender))
.setBot(sender.isBot())
.setKey(accountId + "-" + contactId)
.build(),
tickerLine);
}
maybeAddNotification(
accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser());
accountId, dcChat, msgId, notifData, tickerLine, false, dcChat.isMultiUser());
});
}
@@ -498,7 +562,7 @@ public class NotificationCenter {
int accountId,
DcChat dcChat,
int msgId,
String shortLine,
NotifData notifData,
String tickerLine,
boolean playInChatSound,
boolean isMention) {
@@ -538,20 +602,23 @@ public class NotificationCenter {
// the user may eg. have chosen a different sound
String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat);
LinkedHashMap<Integer, String> messagesForInbox = null;
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
LinkedHashMap<Integer, NotifData> messagesForInbox = null;
if (privacy.isDisplayContact()) {
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
if (accountInbox == null) {
accountInbox = new HashMap<>();
inboxes.put(accountId, accountInbox);
}
LinkedHashMap<Integer, String> messages = accountInbox.get(chatId);
LinkedHashMap<Integer, NotifData> messages = accountInbox.get(chatId);
if (messages == null) {
messages = new LinkedHashMap<>();
accountInbox.put(chatId, messages);
}
messages.put(msgId, shortLine);
if (!privacy.isDisplayMessage()) {
messages.clear();
}
messages.put(msgId, notifData);
messagesForInbox = new LinkedHashMap<>(messages);
}
}
@@ -564,7 +631,7 @@ public class NotificationCenter {
dcContext,
dcChat,
notificationChannel,
shortLine,
notifData.text,
tickerLine,
signal,
messagesForInbox,
@@ -583,7 +650,7 @@ public class NotificationCenter {
String contentText,
String ticker,
boolean signal,
LinkedHashMap<Integer, String> messagesForInbox,
LinkedHashMap<Integer, NotifData> messagesForInbox,
int messageCount,
boolean includeSummary) {
try {
@@ -658,7 +725,7 @@ public class NotificationCenter {
NotificationCompat.Action markAsReadAction =
new NotificationCompat.Action(
R.drawable.check, context.getString(R.string.mark_as_read_short), markReadIntent);
R.drawable.check, context.getString(R.string.mark_as_read), markReadIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NotificationCompat.Action replyAction =
@@ -694,14 +761,42 @@ public class NotificationCenter {
}
}
// Create inbox style (gets visible if the notification is expanded)
if (privacy.isDisplayContact() && privacy.isDisplayMessage() && messagesForInbox != null) {
// Create messaging style
if (privacy.isDisplayContact() && messagesForInbox != null) {
try {
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
for (String line : messagesForInbox.values()) {
inboxStyle.addLine(line);
Intent viewChatIntent = new Intent(context, ShareActivity.class);
viewChatIntent.setAction(Intent.ACTION_SEND);
viewChatIntent.putExtra(ShareActivity.EXTRA_ACC_ID, dcContext.getAccountId());
viewChatIntent.putExtra(ShareActivity.EXTRA_CHAT_ID, dcChat.getId());
ShortcutInfoCompat shortcut =
new ShortcutInfoCompat.Builder(
context, "chat-" + dcContext.getAccountId() + "-" + dcChat.getId())
.setShortLabel(dcChat.getName())
.setLongLived(true)
.setIntent(viewChatIntent)
.build();
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut);
builder.setShortcutInfo(shortcut);
DcContact selfContact = dcContext.getContact(DcContact.DC_CONTACT_ID_SELF);
Person self =
new Person.Builder()
.setName(selfContact.getDisplayName())
.setIcon(getAvatarIcon(selfContact))
.setKey(accountId + "-" + selfContact.getId())
.build();
NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle(self);
if (dcChat.isMultiUser()) {
style.setGroupConversation(true);
style.setConversationTitle(dcChat.getName());
}
builder.setStyle(inboxStyle);
for (Map.Entry<Integer, NotifData> msgEntry : messagesForInbox.entrySet()) {
long timestamp_ms = dcContext.getMsg(msgEntry.getKey()).getSortTimestamp() * 1000;
NotifData notifData = msgEntry.getValue();
style.addMessage(notifData.text, timestamp_ms, notifData.sender);
}
builder.setStyle(style);
} catch (Exception e) {
Log.w(TAG, e);
}
@@ -748,7 +843,7 @@ public class NotificationCenter {
@WorkerThread
private void rebuildNotification(
int accountId, int chatId, LinkedHashMap<Integer, String> messages) {
int accountId, int chatId, LinkedHashMap<Integer, NotifData> messages) {
try {
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accountId);
DcChat dcChat = dcContext.getChat(chatId);
@@ -766,9 +861,9 @@ public class NotificationCenter {
// Get the latest message ID (last entry in LinkedHashMap)
Integer latestMsgId = null;
String lastLine = null;
for (Map.Entry<Integer, String> entry : messages.entrySet()) {
for (Map.Entry<Integer, NotifData> entry : messages.entrySet()) {
latestMsgId = entry.getKey();
lastLine = entry.getValue();
lastLine = entry.getValue().text;
}
if (latestMsgId == null || lastLine == null) {
return;
@@ -797,8 +892,35 @@ public class NotificationCenter {
}
}
public Bitmap getAvatar(DcChat dcChat) {
Recipient recipient = new Recipient(context, dcChat);
private static @Nullable IconCompat getAvatarIcon(byte[] blob) {
if (blob == null) {
return null;
}
ByteArrayInputStream is = new ByteArrayInputStream(blob);
BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon");
Bitmap bitmap = drawable.getBitmap();
return IconCompat.createWithBitmap(bitmap);
}
private @Nullable IconCompat getAvatarIcon(DcChat dcChat) {
Bitmap avatar = getAvatar(dcChat);
return avatar != null ? IconCompat.createWithBitmap(avatar) : null;
}
private @Nullable IconCompat getAvatarIcon(DcContact dcContact) {
Bitmap avatar = getAvatar(dcContact);
return avatar != null ? IconCompat.createWithBitmap(avatar) : null;
}
private @Nullable Bitmap getAvatar(DcChat dcChat) {
return getAvatar(new Recipient(context, dcChat));
}
private @Nullable Bitmap getAvatar(DcContact dcContact) {
return getAvatar(new Recipient(context, dcContact));
}
private @Nullable Bitmap getAvatar(Recipient recipient) {
try {
Drawable drawable;
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
@@ -837,12 +959,12 @@ public class NotificationCenter {
public void removeNotification(int accountId, int chatId, int msgId) {
boolean shouldCancelNotification = false;
boolean removeSummary = false;
LinkedHashMap<Integer, String> remainingMessages = null;
LinkedHashMap<Integer, NotifData> remainingMessages = null;
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
if (accountInbox != null) {
LinkedHashMap<Integer, String> messages = accountInbox.get(chatId);
LinkedHashMap<Integer, NotifData> messages = accountInbox.get(chatId);
if (messages != null) {
messages.remove(msgId);
@@ -875,7 +997,7 @@ public class NotificationCenter {
public void removeNotifications(int accountId, int chatId) {
boolean removeSummary;
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
if (accountInbox == null) {
accountInbox = new HashMap<>();
}
@@ -901,7 +1023,7 @@ public class NotificationCenter {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
String tag = String.valueOf(accountId);
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, NotifData>> accountInbox = inboxes.get(accountId);
notificationManager.cancel(tag, ID_MSG_SUMMARY);
if (accountInbox != null) {
for (Integer chatId : accountInbox.keySet()) {
@@ -3,9 +3,6 @@ package org.thoughtcrime.securesms.preferences;
import static android.app.Activity.RESULT_OK;
import static android.text.InputType.TYPE_TEXT_VARIATION_URI;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_BCC_SELF;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_MVBOX_MOVE;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_ONLY_FETCH_MVBOX;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_SHOW_EMAILS;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_STATS_SENDING;
import android.content.Context;
@@ -22,7 +19,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.CheckBoxPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import java.io.File;
import java.io.FileOutputStream;
@@ -40,17 +36,13 @@ import org.thoughtcrime.securesms.relay.RelayListActivity;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ScreenLockUtil;
import org.thoughtcrime.securesms.util.StreamUtil;
import org.thoughtcrime.securesms.util.Util;
public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
implements DcEventCenter.DcEventDelegate {
private static final String TAG = "AdvancedPreferenceFragment";
private ListPreference showEmails;
CheckBoxPreference selfReportingCheckbox;
CheckBoxPreference multiDeviceCheckbox;
CheckBoxPreference mvboxMoveCheckbox;
CheckBoxPreference onlyFetchMvboxCheckbox;
private ActivityResultLauncher<Intent> screenLockLauncher;
@Override
@@ -66,16 +58,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
}
});
showEmails = (ListPreference) this.findPreference("pref_show_emails");
if (showEmails != null) {
showEmails.setOnPreferenceChangeListener(
(preference, newValue) -> {
updateListSummary(preference, newValue);
dcContext.setConfigInt(CONFIG_SHOW_EMAILS, Util.objectToInt(newValue));
return true;
});
}
multiDeviceCheckbox = (CheckBoxPreference) this.findPreference("pref_bcc_self");
if (multiDeviceCheckbox != null) {
multiDeviceCheckbox.setOnPreferenceChangeListener(
@@ -100,40 +82,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
});
}
mvboxMoveCheckbox = (CheckBoxPreference) this.findPreference("pref_mvbox_move");
if (mvboxMoveCheckbox != null) {
mvboxMoveCheckbox.setOnPreferenceChangeListener(
(preference, newValue) -> {
boolean enabled = (Boolean) newValue;
dcContext.setConfigInt(CONFIG_MVBOX_MOVE, enabled ? 1 : 0);
return true;
});
}
onlyFetchMvboxCheckbox = this.findPreference("pref_only_fetch_mvbox");
if (onlyFetchMvboxCheckbox != null) {
onlyFetchMvboxCheckbox.setOnPreferenceChangeListener(
((preference, newValue) -> {
final boolean enabled = (Boolean) newValue;
if (enabled) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.pref_imap_folder_warn_disable_defaults)
.setPositiveButton(
R.string.ok,
(dialogInterface, i) -> {
dcContext.setConfigInt(CONFIG_ONLY_FETCH_MVBOX, 1);
((CheckBoxPreference) preference).setChecked(true);
})
.setNegativeButton(R.string.cancel, null)
.show();
return false;
} else {
dcContext.setConfigInt(CONFIG_ONLY_FETCH_MVBOX, 0);
return true;
}
}));
}
Preference submitDebugLog = this.findPreference("pref_view_log");
if (submitDebugLog != null) {
submitDebugLog.setOnPreferenceClickListener(new ViewLogListener());
@@ -189,10 +137,6 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
return true;
}));
}
if (dcContext.isChatmail()) {
findPreference("pref_category_legacy").setVisible(false);
}
}
@Override
@@ -207,14 +151,8 @@ public class AdvancedPreferenceFragment extends ListSummaryPreferenceFragment
((ApplicationPreferencesActivity) requireActivity()).getSupportActionBar())
.setTitle(R.string.menu_advanced);
String value = Integer.toString(dcContext.getConfigInt("show_emails"));
showEmails.setValue(value);
updateListSummary(showEmails, value);
selfReportingCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_STATS_SENDING));
multiDeviceCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_BCC_SELF));
mvboxMoveCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_MVBOX_MOVE));
onlyFetchMvboxCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_ONLY_FETCH_MVBOX));
}
protected File copyToCacheDir(Uri uri) throws IOException {
@@ -39,6 +39,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
private CheckBoxPreference ignoreBattery;
private CheckBoxPreference notificationsEnabled;
private CheckBoxPreference mentionNotifEnabled;
private CheckBoxPreference notifyCalls;
private CheckBoxPreference reliableService;
private ActivityResultLauncher<Intent> ringtonePickerLauncher;
@@ -137,6 +138,16 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
return true;
});
}
notifyCalls = this.findPreference("pref_notify_calls");
if (notifyCalls != null) {
notifyCalls.setOnPreferenceChangeListener(
(preference, newValue) -> {
boolean enabled = (Boolean) newValue;
dcContext.setConfig("who_can_call_me", enabled ? "1" : "2");
return true;
});
}
}
@Override
@@ -156,6 +167,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
notificationsEnabled.setChecked(!dcContext.isMuted());
notificationsEnabled.setSummary(getSummary(getContext(), false));
mentionNotifEnabled.setChecked(dcContext.isMentionsEnabled());
notifyCalls.setChecked(!"2".equals(dcContext.getConfig("who_can_call_me")));
// set without altering "unset" state of the preference
reliableService.setOnPreferenceChangeListener(null);
@@ -26,7 +26,6 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
private CheckBoxPreference readReceiptsCheckbox;
private ListPreference autoDelDevice;
private ListPreference autoDelServer;
@Override
public void onCreate(Bundle paramBundle) {
@@ -39,13 +38,7 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
.setOnPreferenceClickListener(new BlockedContactsClickListener());
autoDelDevice = findPreference("autodel_device");
autoDelDevice.setOnPreferenceChangeListener(new AutodelChangeListener("delete_device_after"));
autoDelServer = findPreference("autodel_server");
autoDelServer.setOnPreferenceChangeListener(new AutodelChangeListener("delete_server_after"));
if (dcContext.isChatmail()) {
autoDelServer.setVisible(false);
}
autoDelDevice.setOnPreferenceChangeListener(new AutodelChangeListener());
Preference screenSecurity = this.findPreference(Prefs.SCREEN_SECURITY_PREF);
screenSecurity.setOnPreferenceChangeListener(new ScreenShotSecurityListener());
@@ -68,16 +61,7 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
}
private void initAutodelFromCore() {
String value = Integer.toString(dcContext.getConfigInt("delete_server_after"));
autoDelServer.setValue(value);
updateListSummary(
autoDelServer,
value,
(value.equals("0") || dcContext.isChatmail())
? null
: getString(R.string.autodel_server_enabled_hint));
value = Integer.toString(dcContext.getConfigInt("delete_device_after"));
String value = Integer.toString(dcContext.getConfigInt("delete_device_after"));
autoDelDevice.setValue(value);
updateListSummary(autoDelDevice, value);
}
@@ -109,19 +93,14 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
}
private class AutodelChangeListener implements Preference.OnPreferenceChangeListener {
private final String coreKey;
AutodelChangeListener(String coreKey) {
this.coreKey = coreKey;
}
private final String coreKey = "delete_device_after";
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int timeout = Util.objectToInt(newValue);
Context context = preference.getContext();
boolean fromServer = coreKey.equals("delete_server_after");
if (timeout > 0 && !(fromServer && dcContext.isChatmail())) {
int delCount = DcHelper.getContext(context).estimateDeletionCount(fromServer, timeout);
if (timeout > 0) {
int delCount = DcHelper.getContext(context).estimateDeletionCount(false, timeout);
View gl = View.inflate(getActivity(), R.layout.dialog_with_checkbox, null);
CheckBox confirmCheckbox = gl.findViewById(R.id.dialog_checkbox);
@@ -132,8 +111,7 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
// "OK" and "Cancel" buttons would not be show. So, put the message into our custom view:
msg.setText(
String.format(
context.getString(
fromServer ? R.string.autodel_server_ask : R.string.autodel_device_ask),
context.getString(R.string.autodel_device_ask),
delCount,
getSelectedSummary(preference, newValue)));
confirmCheckbox.setText(R.string.autodel_confirm);
@@ -158,23 +136,6 @@ public class PrivacyPreferenceFragment extends ListSummaryPreferenceFragment {
// :)
.setOnCancelListener(dialog -> initAutodelFromCore())
.show();
} else if (fromServer
&& timeout
== 1 /*at once, using a constant that cannot be used in .xml would weaken grep ability*/) {
new AlertDialog.Builder(context)
.setTitle(R.string.autodel_server_warn_multi_device_title)
.setMessage(R.string.autodel_server_warn_multi_device)
.setPositiveButton(
android.R.string.ok,
(dialog, whichButton) -> {
dcContext.setConfigInt(coreKey, timeout);
initAutodelFromCore();
})
.setNegativeButton(
android.R.string.cancel, (dialog, whichButton) -> initAutodelFromCore())
.setCancelable(true)
.setOnCancelListener(dialog -> initAutodelFromCore())
.show();
} else {
updateListSummary(preference, newValue);
dcContext.setConfigInt(coreKey, timeout);
@@ -15,10 +15,12 @@ import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.b44t.messenger.DcContext;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
@@ -48,7 +50,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
private static final int TAB_SCAN = 1;
private TabLayout tabLayout;
private ViewPager viewPager;
private ViewPager2 viewPager;
private QrShowFragment qrShowFragment;
private boolean scanRelay;
@@ -68,7 +70,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
qrShowFragment = new QrShowFragment(this);
tabLayout = ViewUtil.findById(this, R.id.tab_layout);
viewPager = ViewUtil.findById(this, R.id.pager);
ProfilePagerAdapter adapter = new ProfilePagerAdapter(this, getSupportFragmentManager());
ProfilePagerAdapter adapter = new ProfilePagerAdapter(this);
viewPager.setAdapter(adapter);
setSupportActionBar(ViewUtil.findById(this, R.id.toolbar));
@@ -76,29 +78,24 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
getSupportActionBar().setTitle(scanRelay ? R.string.add_transport : R.string.menu_new_contact);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
viewPager.setCurrentItem(scanRelay ? TAB_SCAN : TAB_SHOW);
viewPager.setCurrentItem(scanRelay ? TAB_SCAN : TAB_SHOW, false);
if (scanRelay) tabLayout.setVisibility(View.GONE);
viewPager.addOnPageChangeListener(
new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(
int position, float positionOffset, int positionOffsetPixels) {}
viewPager.registerOnPageChangeCallback(
new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
QrActivity.this.invalidateOptionsMenu();
checkPermissions(position, adapter, viewPager);
}
@Override
public void onPageScrollStateChanged(int state) {}
});
tabLayout.setupWithViewPager(viewPager);
new TabLayoutMediator(
tabLayout, viewPager, (tab, position) -> tab.setText(adapter.getPageTitle(position)))
.attach();
}
private void checkPermissions(int position, ProfilePagerAdapter adapter, ViewPager viewPager) {
private void checkPermissions(int position, ProfilePagerAdapter adapter, ViewPager2 viewPager) {
if (position == TAB_SCAN) {
Permissions.with(QrActivity.this)
.request(Manifest.permission.CAMERA)
@@ -106,7 +103,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(
() ->
((QrScanFragment) adapter.getItem(TAB_SCAN))
((QrScanFragment) adapter.createFragment(TAB_SCAN))
.handleQrScanWithPermissions(QrActivity.this))
.onAnyDenied(
() -> {
@@ -124,10 +121,12 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
DcContext dcContext = DcHelper.getContext(this);
menu.clear();
getMenuInflater().inflate(R.menu.qr_show, menu);
menu.findItem(R.id.new_classic_contact)
.setVisible(!scanRelay && !DcHelper.getContext(this).isChatmail());
.setVisible(!scanRelay && dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0);
Util.redMenuItem(menu, R.id.withdraw);
if (tabLayout.getSelectedTabPosition() == TAB_SCAN) {
@@ -228,47 +227,28 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
viewPager.setCurrentItem(TAB_SCAN);
}
private class ProfilePagerAdapter extends FragmentStatePagerAdapter {
private final QrActivity activity;
ProfilePagerAdapter(QrActivity activity, FragmentManager fragmentManager) {
super(fragmentManager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.activity = activity;
private class ProfilePagerAdapter extends FragmentStateAdapter {
ProfilePagerAdapter(FragmentActivity activity) {
super(activity);
}
@NonNull
@Override
public Fragment getItem(int position) {
Fragment fragment;
switch (position) {
case TAB_SHOW:
fragment = activity.qrShowFragment;
break;
default:
fragment = new QrScanFragment();
break;
public Fragment createFragment(int position) {
if (position == TAB_SHOW) {
qrShowFragment = new QrShowFragment(QrActivity.this);
return qrShowFragment;
}
return fragment;
return new QrScanFragment();
}
@Override
public int getCount() {
public int getItemCount() {
return 2;
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case TAB_SHOW:
return getString(R.string.qrshow_title);
default:
return getString(R.string.qrscan_title);
}
private CharSequence getPageTitle(int position) {
return getString(position == TAB_SHOW ? R.string.qrshow_title : R.string.qrscan_title);
}
}
}
@@ -314,6 +314,12 @@ public class QrCodeHandler {
private void showFingerprintOrQrSuccess(
AlertDialog.Builder builder, DcLot qrParsed, String name) {
if (qrParsed.getState() == DcContext.DC_QR_ADDR
&& dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 1) {
DcHelper.prepareInvalidUnencryptedDialog(activity, builder);
return;
}
@StringRes
int resId =
qrParsed.getState() == DcContext.DC_QR_ADDR
@@ -23,6 +23,7 @@ import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.SwitchCompat;
import androidx.constraintlayout.widget.Group;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
@@ -72,9 +73,13 @@ public class EditRelayActivity extends BaseActionBarActivity
private ProgressDialog progressDialog;
private boolean cancelled = false;
private View imapFolderLayout;
private boolean showImapFolder = false;
Spinner imapSecurity;
Spinner smtpSecurity;
Spinner certCheck;
private SwitchCompat enforceE2eeSwitch;
Rpc rpc;
int accId;
@@ -100,9 +105,11 @@ public class EditRelayActivity extends BaseActionBarActivity
advancedGroup = findViewById(R.id.advanced_group);
advancedIcon = findViewById(R.id.advanced_icon);
imapFolderLayout = findViewById(R.id.imap_folder);
TextView advancedTextView = findViewById(R.id.advanced_text);
TextInputEditText imapServerInput = findViewById(R.id.imap_server_text);
TextInputEditText imapPortInput = findViewById(R.id.imap_port_text);
TextInputEditText imapFolderInput = findViewById(R.id.imap_folder_text);
TextInputEditText smtpServerInput = findViewById(R.id.smtp_server_text);
TextInputEditText smtpPortInput = findViewById(R.id.smtp_port_text);
TextView viewLogText = findViewById(R.id.view_log_button);
@@ -110,6 +117,7 @@ public class EditRelayActivity extends BaseActionBarActivity
imapSecurity = findViewById(R.id.imap_security);
smtpSecurity = findViewById(R.id.smtp_security);
certCheck = findViewById(R.id.cert_check);
enforceE2eeSwitch = findViewById(R.id.enforce_e2ee_switch);
String addr = getIntent().getStringExtra(EXTRA_ADDR);
EnteredLoginParam config = null;
@@ -173,6 +181,10 @@ public class EditRelayActivity extends BaseActionBarActivity
boolean expandAdvanced = false;
int intVal;
intVal = getContext(this).getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION);
enforceE2eeSwitch.setChecked(intVal == 1);
expandAdvanced = expandAdvanced || intVal == 0;
if (config != null) { // configured
emailInput.setText(config.addr);
if (!TextUtils.isEmpty(config.addr)) {
@@ -190,6 +202,12 @@ public class EditRelayActivity extends BaseActionBarActivity
if (config.imapPort != null) imapPortInput.setText(config.imapPort.toString());
expandAdvanced = expandAdvanced || config.imapPort != null;
showImapFolder = !TextUtils.isEmpty(config.imapFolder);
if (showImapFolder) {
imapFolderInput.setText(config.imapFolder);
expandAdvanced = true;
}
intVal = socketSecurityToInt(config.imapSecurity);
imapSecurity.setSelection(ViewUtil.checkBounds(intVal, imapSecurity));
expandAdvanced = expandAdvanced || intVal != 0;
@@ -386,9 +404,11 @@ public class EditRelayActivity extends BaseActionBarActivity
boolean advancedViewVisible = advancedGroup.getVisibility() == View.VISIBLE;
if (advancedViewVisible) {
advancedGroup.setVisibility(View.GONE);
imapFolderLayout.setVisibility(View.GONE);
advancedIcon.setRotation(45);
} else {
advancedGroup.setVisibility(View.VISIBLE);
imapFolderLayout.setVisibility(showImapFolder ? View.VISIBLE : View.GONE);
advancedIcon.setRotation(0);
}
}
@@ -491,6 +511,7 @@ public class EditRelayActivity extends BaseActionBarActivity
param.password = getParam(R.id.password_text, false);
param.imapServer = getParam(R.id.imap_server_text, true);
param.imapPort = Util.objectToInt(getParam(R.id.imap_port_text, true));
param.imapFolder = getParam(R.id.imap_folder_text, true);
param.imapSecurity = socketSecurityFromInt(imapSecurity.getSelectedItemPosition());
param.imapUser = getParam(R.id.imap_login_text, false);
param.smtpServer = getParam(R.id.smtp_server_text, true);
@@ -500,9 +521,12 @@ public class EditRelayActivity extends BaseActionBarActivity
param.smtpPassword = getParam(R.id.smtp_password_text, false);
param.certificateChecks = certificateChecksFromInt(certCheck.getSelectedItemPosition());
final String forceEncryption = enforceE2eeSwitch.isChecked() ? "1" : "0";
new Thread(
() -> {
try {
rpc.setConfig(accId, DcHelper.CONFIG_FORCE_ENCRYPTION, forceEncryption);
rpc.addOrUpdateTransport(accId, param);
DcHelper.getEventCenter(this).endCaptureNextError();
progressDialog.dismiss();
@@ -0,0 +1,91 @@
package org.thoughtcrime.securesms.search;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import org.thoughtcrime.securesms.R;
public class QrInviteData {
private final String displayTitle;
private final String displaySubtitle;
private final String rawQrString;
private final int contactId;
private QrInviteData(
@NonNull String displayTitle,
@NonNull String displaySubtitle,
@NonNull String rawQrString,
int contactId) {
this.displayTitle = displayTitle;
this.displaySubtitle = displaySubtitle;
this.rawQrString = rawQrString;
this.contactId = contactId;
}
@Nullable
public static QrInviteData from(
@NonNull Context context,
@NonNull DcContext dcContext,
@NonNull DcLot qrParsed,
@NonNull String rawQrString) {
int state = qrParsed.getState();
String title;
String subtitle;
int contactId = 0;
switch (state) {
case DcContext.DC_QR_ASK_VERIFYCONTACT:
case DcContext.DC_QR_FPR_OK:
case DcContext.DC_QR_ADDR:
contactId = qrParsed.getId();
DcContact contact = dcContext.getContact(contactId);
title = contact.getDisplayName();
subtitle = context.getString(R.string.start_chat);
break;
case DcContext.DC_QR_ASK_VERIFYGROUP:
title = qrParsed.getText1();
subtitle = context.getString(R.string.join_group);
break;
case DcContext.DC_QR_ASK_JOIN_BROADCAST:
title = qrParsed.getText1();
subtitle = context.getString(R.string.join_channel);
break;
case DcContext.DC_QR_ACCOUNT:
case DcContext.DC_QR_LOGIN:
title = qrParsed.getText1();
subtitle = context.getString(R.string.add_transport);
break;
case DcContext.DC_QR_PROXY:
title = qrParsed.getText1();
subtitle = context.getString(R.string.proxy_use_proxy);
break;
default:
return null;
}
return new QrInviteData(title, subtitle, rawQrString, contactId);
}
@NonNull
public String getDisplayTitle() {
return displayTitle;
}
@NonNull
public String getDisplaySubtitle() {
return displaySubtitle;
}
@NonNull
public String getRawQrString() {
return rawQrString;
}
public int getContactId() {
return contactId;
}
}
@@ -200,6 +200,14 @@ public class SearchFragment extends BaseConversationListFragment
}
}
@Override
public void onInvitationClicked(@NonNull String rawQrString) {
ConversationListActivity conversationList = (ConversationListActivity) getActivity();
if (conversationList != null) {
conversationList.handleQrFromSearch(rawQrString);
}
}
public void updateSearchQuery(@NonNull String query) {
if (viewModel != null) {
viewModel.updateQuery(query);
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
class SearchListAdapter
extends BaseConversationListAdapter<SearchListAdapter.SearchResultViewHolder>
implements StickyHeaderDecoration.StickyHeaderAdapter<SearchListAdapter.HeaderViewHolder> {
private static final int TYPE_QR_INVITE = 0;
private static final int TYPE_CHATS = 1;
private static final int TYPE_CONTACTS = 2;
private static final int TYPE_MESSAGES = 3;
@@ -57,6 +58,14 @@ class SearchListAdapter
@Override
public void onBindViewHolder(@NonNull SearchResultViewHolder holder, int position) {
if (isQrInvitePosition(position)) {
QrInviteData inviteData = searchResult.getQrInviteData();
if (inviteData != null) {
holder.bind(inviteData, glideRequests, eventListener);
}
return;
}
DcChatlist.Item conversationResult = getConversationResult(position);
if (conversationResult != null) {
@@ -97,7 +106,9 @@ class SearchListAdapter
@Override
public long getHeaderId(int position) {
if (getConversationResult(position) != null) {
if (isQrInvitePosition(position)) {
return TYPE_QR_INVITE;
} else if (getConversationResult(position) != null) {
return TYPE_CHATS;
} else if (getContactResult(position) != null) {
return TYPE_CONTACTS;
@@ -116,33 +127,39 @@ class SearchListAdapter
@Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
int headerType = (int) getHeaderId(position);
int textId = R.plurals.n_messages;
int count = 1;
boolean maybeLimitedTo1000 = false;
String title;
switch (headerType) {
case TYPE_CHATS:
textId = R.plurals.n_chats;
count = searchResult.getChats().getCnt();
break;
case TYPE_CONTACTS:
textId = R.plurals.n_contacts;
count = searchResult.getContacts().length;
break;
case TYPE_MESSAGES:
textId = R.plurals.n_messages;
count = searchResult.getMessages().length;
maybeLimitedTo1000 =
count == 1000; // a count of 1000 results may be limited, see documentation of
// dc_search_msgs()
break;
}
if (headerType == TYPE_QR_INVITE) {
title = context.getString(R.string.link);
} else {
int textId = R.plurals.n_messages;
int count = 1;
boolean maybeLimitedTo1000 = false;
String title = context.getResources().getQuantityString(textId, count, count);
if (maybeLimitedTo1000) {
title =
title.replace(
"000", "000+"); // skipping the first digit allows formattings as "1.000" or "1,000"
switch (headerType) {
case TYPE_CHATS:
textId = R.plurals.n_chats;
count = searchResult.getChats().getCnt();
break;
case TYPE_CONTACTS:
textId = R.plurals.n_contacts;
count = searchResult.getContacts().length;
break;
case TYPE_MESSAGES:
textId = R.plurals.n_messages;
count = searchResult.getMessages().length;
maybeLimitedTo1000 =
count == 1000; // a count of 1000 results may be limited, see documentation of
// dc_search_msgs()
break;
}
title = context.getResources().getQuantityString(textId, count, count);
if (maybeLimitedTo1000) {
title =
title.replace(
"000", "000+"); // skipping the first digit allows formattings as "1.000" or "1,000"
}
}
viewHolder.bind(title);
}
@@ -160,10 +177,19 @@ class SearchListAdapter
notifyDataSetChanged();
}
private int getQrInviteCount() {
return searchResult.getQrInviteData() != null ? 1 : 0;
}
private boolean isQrInvitePosition(int position) {
return position == 0 && searchResult.getQrInviteData() != null;
}
@Nullable
private DcChatlist.Item getConversationResult(int position) {
if (position < searchResult.getChats().getCnt()) {
return searchResult.getChats().getItem(position);
int offset = position - getQrInviteCount();
if (offset >= 0 && offset < searchResult.getChats().getCnt()) {
return searchResult.getChats().getItem(offset);
}
return null;
}
@@ -185,7 +211,7 @@ class SearchListAdapter
}
private int getFirstContactIndex() {
return searchResult.getChats().getCnt();
return getQrInviteCount() + searchResult.getChats().getCnt();
}
private int getFirstMessageIndex() {
@@ -200,6 +226,8 @@ class SearchListAdapter
void onContactClicked(@NonNull DcContact contact);
void onMessageClicked(@NonNull DcMsg message);
void onInvitationClicked(@NonNull String rawQrString);
}
static class SearchResultViewHolder extends RecyclerView.ViewHolder {
@@ -257,9 +285,20 @@ class SearchListAdapter
root.setOnClickListener(view -> eventListener.onMessageClicked(messageResult));
}
void bind(
@NonNull QrInviteData inviteData,
@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener) {
root.bind(inviteData, glideRequests);
root.setOnClickListener(
view -> eventListener.onInvitationClicked(inviteData.getRawQrString()));
root.setOnLongClickListener(null);
}
void recycle() {
root.unbind();
root.setOnClickListener(null);
root.setOnLongClickListener(null);
}
}
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.b44t.messenger.DcChatlist;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.search.model.SearchResult;
import org.thoughtcrime.securesms.util.Util;
@@ -18,13 +19,16 @@ class SearchViewModel extends ViewModel {
private static final String TAG = "SearchViewModel";
private final ObservingLiveData searchResult;
private String lastQuery;
private final Context appContext;
private final DcContext dcContext;
private boolean forwarding = false;
private final Object bgSearchLock = new Object();
private boolean inBgSearch;
private boolean needsAnotherBgSearch;
SearchViewModel(@NonNull Context context) {
this.dcContext = DcHelper.getContext(context.getApplicationContext());
this.appContext = context.getApplicationContext();
this.dcContext = DcHelper.getContext(appContext);
this.searchResult = new ObservingLiveData();
}
@@ -42,27 +46,29 @@ class SearchViewModel extends ViewModel {
}
public void updateQuery() {
if (inBgSearch) {
needsAnotherBgSearch = true;
Log.i(TAG, "... search call debounced");
} else {
synchronized (bgSearchLock) {
if (inBgSearch) {
Log.i(TAG, "... search call debounced");
needsAnotherBgSearch = true;
return;
}
inBgSearch = true;
Util.runOnBackground(
() -> {
Util.sleep(100);
needsAnotherBgSearch = false;
queryAndCallback(lastQuery, searchResult::postValue);
while (needsAnotherBgSearch) {
Util.sleep(100);
needsAnotherBgSearch = false;
Log.i(TAG, "... executing debounced search call");
queryAndCallback(lastQuery, searchResult::postValue);
}
inBgSearch = false;
});
}
Util.runOnBackground(
() -> {
while (true) {
Log.i(TAG, "... executing debounced search call");
queryAndCallback(lastQuery, searchResult::postValue);
synchronized (bgSearchLock) {
if (!needsAnotherBgSearch) {
inBgSearch = false;
return;
}
needsAnotherBgSearch = false;
}
Util.sleep(100);
}
});
}
private void queryAndCallback(@NonNull String query, @NonNull SearchViewModel.Callback callback) {
@@ -73,6 +79,12 @@ class SearchViewModel extends ViewModel {
return;
}
QrInviteData qrInviteData = null;
if (!forwarding && query.contains(":")) {
DcLot qrParsed = dcContext.checkQr(query);
qrInviteData = QrInviteData.from(appContext, dcContext, qrParsed, query);
}
// #1 search for chats
long startMs = System.currentTimeMillis();
DcChatlist conversations =
@@ -83,7 +95,8 @@ class SearchViewModel extends ViewModel {
// #2 search for contacts
if (!query.equals(lastQuery) && overallCnt > 0) {
Log.i(TAG, "... skipping getContacts() and searchMsgs(), more recent search pending");
callback.onResult(new SearchResult(query, new int[0], conversations, new int[0]));
callback.onResult(
new SearchResult(query, new int[0], conversations, new int[0], qrInviteData));
return;
}
@@ -95,19 +108,19 @@ class SearchViewModel extends ViewModel {
// #3 search for messages
if (forwarding) {
Log.i(TAG, "... searchMsgs() disabled by caller");
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
return;
}
if (query.length() <= 1) {
Log.i(TAG, "... skipping searchMsgs(), string too short");
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
return;
}
if (!query.equals(lastQuery) && overallCnt > 0) {
Log.i(TAG, "... skipping searchMsgs(), more recent search pending");
callback.onResult(new SearchResult(query, contacts, conversations, new int[0]));
callback.onResult(new SearchResult(query, contacts, conversations, new int[0], qrInviteData));
return;
}
@@ -115,7 +128,7 @@ class SearchViewModel extends ViewModel {
int[] messages = dcContext.searchMsgs(0, query);
Log.i(TAG, "⏰ searchMsgs(" + query + "): " + (System.currentTimeMillis() - startMs) + "ms");
callback.onResult(new SearchResult(query, contacts, conversations, messages));
callback.onResult(new SearchResult(query, contacts, conversations, messages, qrInviteData));
}
@NonNull
@@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.search.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.b44t.messenger.DcChatlist;
import org.thoughtcrime.securesms.search.QrInviteData;
/**
* Represents an all-encompassing search result that can contain various result for different
@@ -16,16 +18,27 @@ public class SearchResult {
private final int[] contacts;
private final DcChatlist conversations;
private final int[] messages;
private final QrInviteData qrInviteData;
public SearchResult(
@NonNull String query,
@NonNull int[] contacts,
@NonNull DcChatlist conversations,
@NonNull int[] messages) {
this(query, contacts, conversations, messages, null);
}
public SearchResult(
@NonNull String query,
@NonNull int[] contacts,
@NonNull DcChatlist conversations,
@NonNull int[] messages,
@Nullable QrInviteData qrInviteData) {
this.query = query;
this.contacts = contacts;
this.conversations = conversations;
this.messages = messages;
this.qrInviteData = qrInviteData;
}
public int[] getContacts() {
@@ -44,8 +57,16 @@ public class SearchResult {
return query;
}
@Nullable
public QrInviteData getQrInviteData() {
return qrInviteData;
}
public int size() {
return contacts.length + conversations.getCnt() + messages.length;
return (qrInviteData != null ? 1 : 0)
+ contacts.length
+ conversations.getCnt()
+ messages.length;
}
public boolean isEmpty() {
@@ -57,22 +57,26 @@ public class LongClickCopySpan extends ClickableSpan {
DcContext dcContext = DcHelper.getContext(activity);
int contactId = dcContext.lookupContactIdByAddr(addr);
if (contactId == 0 && dcContext.mayBeValidAddr(addr)) {
contactId = dcContext.createContact(null, addr);
}
DcContact contact = dcContext.getContact(contactId);
if (contact.getId() != 0
DcContact contact = (contactId != 0) ? dcContext.getContact(contactId) : null;
if (contact != null
&& !contact.isBlocked()
&& dcContext.getChatIdByContactId(contact.getId()) != 0) {
openChat(activity, contact);
} else if (contact == null
&& dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 1) {
DcHelper.showInvalidUnencryptedDialog(activity);
} else {
String name = contact != null ? contact.getDisplayName() : addr;
new AlertDialog.Builder(activity)
.setMessage(
activity.getString(R.string.ask_start_chat_with, contact.getDisplayName()))
.setMessage(activity.getString(R.string.ask_start_chat_with, name))
.setPositiveButton(
android.R.string.ok,
(dialog, which) -> {
openChat(activity, contact);
openChat(
activity,
contact == null
? dcContext.getContact(dcContext.createContact(null, addr))
: contact);
})
.setNegativeButton(R.string.cancel, null)
.show();
@@ -151,7 +151,6 @@ public class SendRelayedMessageUtil {
message = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
} else if ("sticker".equals(type)) {
message = new DcMsg(dcContext, DcMsg.DC_MSG_STICKER);
message.forceSticker();
} else if ("image".equals(type) || MediaUtil.isImageType(mimeType)) {
message = new DcMsg(dcContext, DcMsg.DC_MSG_IMAGE);
} else if ("audio".equals(type) || MediaUtil.isAudioType(mimeType)) {
@@ -15,7 +15,6 @@ public class ShareUtil {
private static final String FORWARDED_MESSAGE_ACCID = "forwarded_message_accid";
private static final String FORWARDED_MESSAGE_IDS = "forwarded_message_ids";
private static final String SHARED_URIS = "shared_uris";
private static final String SHARED_CONTACT_ID = "shared_contact_id";
private static final String IS_SHARING = "is_sharing";
private static final String IS_FROM_WEBXDC = "is_from_webxdc";
private static final String SHARED_TITLE = "shared_title";
@@ -69,7 +68,7 @@ public class ShareUtil {
}
}
static int[] getForwardedMessageIDs(Activity activity) {
public static int[] getForwardedMessageIDs(Activity activity) {
try {
return activity.getIntent().getIntArrayExtra(FORWARDED_MESSAGE_IDS);
} catch (NullPointerException npe) {
@@ -112,15 +111,6 @@ public class ShareUtil {
}
}
public static int getSharedContactId(Activity activity) {
try {
return activity.getIntent().getIntExtra(SHARED_CONTACT_ID, 0);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
public static String getSharedText(Activity activity) {
try {
return activity.getIntent().getStringExtra(TEXT_EXTRA);
@@ -142,7 +132,6 @@ public class ShareUtil {
activity.getIntent().removeExtra(FORWARDED_MESSAGE_ACCID);
activity.getIntent().removeExtra(FORWARDED_MESSAGE_IDS);
activity.getIntent().removeExtra(SHARED_URIS);
activity.getIntent().removeExtra(SHARED_CONTACT_ID);
activity.getIntent().removeExtra(IS_SHARING);
activity.getIntent().removeExtra(DIRECT_SHARING_CHAT_ID);
activity.getIntent().removeExtra(TEXT_EXTRA);
@@ -167,9 +156,6 @@ public class ShareUtil {
if (!getSharedUris(currentActivity).isEmpty()) {
newActivityIntent.putParcelableArrayListExtra(SHARED_URIS, getSharedUris(currentActivity));
}
if (getSharedContactId(currentActivity) != 0) {
newActivityIntent.putExtra(SHARED_CONTACT_ID, getSharedContactId(currentActivity));
}
if (getSharedText(currentActivity) != null) {
newActivityIntent.putExtra(TEXT_EXTRA, getSharedText(currentActivity));
}
@@ -220,16 +206,7 @@ public class ShareUtil {
composeIntent.putExtra(IS_SHARING, true);
}
public static void setSharedContactId(Intent composeIntent, int contactId) {
composeIntent.putExtra(SHARED_CONTACT_ID, contactId);
composeIntent.putExtra(IS_SHARING, true);
}
public static void setSharedTitle(Intent composeIntent, String text) {
composeIntent.putExtra(SHARED_TITLE, text);
}
public static void setDirectSharing(Intent composeIntent, int chatId) {
composeIntent.putExtra(DIRECT_SHARING_CHAT_ID, chatId);
}
}
+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>
+23 -23
View File
@@ -24,7 +24,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
android:contentDescription="Contact photo"
android:importantForAccessibility="no"
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Full"
app:layout_constraintWidth_percent="0.4"
app:layout_constraintDimensionRatio="1:1"
@@ -44,7 +44,7 @@
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:background="@color/calls_dark_surface"
android:background="@android:color/black"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
@@ -61,7 +61,7 @@
android:id="@+id/display_name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/calls_dark_on_surface"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="John Doe" />
@@ -70,7 +70,7 @@
android:id="@+id/status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/calls_dark_on_surface_variant"
android:textColor="@android:color/white"
android:textSize="14sp"
tools:text="Connecting..." />
@@ -115,7 +115,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="Caller photo"
android:importantForAccessibility="no"
tools:src="@drawable/ic_person" />
</com.google.android.material.card.MaterialCardView>
@@ -141,7 +141,7 @@
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Audio"
android:text="@string/audio"
android:textSize="14sp"
android:minWidth="100dp"
app:icon="@drawable/ic_call"
@@ -153,7 +153,7 @@
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Video"
android:text="@string/video"
android:textSize="14sp"
android:minWidth="100dp"
app:icon="@drawable/ic_videocam_on"
@@ -185,7 +185,7 @@
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="Decline"
android:text="@string/end_call"
android:textSize="15sp"
app:icon="@drawable/ic_call_end"
app:iconGravity="textStart"
@@ -199,7 +199,7 @@
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Answer"
android:text="@string/answer_call"
android:textSize="15sp"
app:icon="@drawable/ic_call"
app:iconGravity="textStart"
@@ -241,9 +241,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_mic_on"
android:contentDescription="Toggle microphone"
app:tint="@color/calls_dark_on_surface"
app:backgroundTint="@color/calls_dark_surface_3"
android:contentDescription="@string/mute"
app:tint="@color/calls_button_foreground"
app:backgroundTint="@color/calls_button_background"
app:fabSize="normal"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
app:borderWidth="0dp" />
@@ -263,9 +263,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_videocam_on"
android:contentDescription="Toggle camera"
app:tint="@color/calls_dark_on_surface"
app:backgroundTint="@color/calls_dark_surface_3"
android:contentDescription="@string/toggle_camera"
app:tint="@color/calls_button_foreground"
app:backgroundTint="@color/calls_button_background"
app:fabSize="normal"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
app:borderWidth="0dp" />
@@ -285,8 +285,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_call_end"
android:contentDescription="End call"
app:tint="@android:color/white"
android:contentDescription="@string/end_call2"
app:tint="@color/calls_button_foreground"
app:backgroundTint="@color/calls_red"
app:fabSize="normal"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
@@ -307,9 +307,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_volume_up"
android:contentDescription="Audio output"
app:tint="@color/calls_dark_on_surface"
app:backgroundTint="@color/calls_dark_surface_3"
android:contentDescription="@string/audio"
app:tint="@color/calls_button_foreground"
app:backgroundTint="@color/calls_button_background"
app:fabSize="normal"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
app:borderWidth="0dp" />
@@ -329,9 +329,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_switch_camera"
android:contentDescription="Switch camera"
app:tint="@color/calls_dark_on_surface"
app:backgroundTint="@color/calls_dark_surface_3"
android:contentDescription="@string/switch_camera"
app:tint="@color/calls_button_foreground"
app:backgroundTint="@color/calls_button_background"
app:fabSize="normal"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
app:borderWidth="0dp" />
+31 -3
View File
@@ -129,7 +129,7 @@
android:visibility="gone"
tools:visibility="visible"
app:constraint_referenced_ids="inbox, imap_login, imap_server, imap_port, imap_security_label, imap_security, outbox_view_spacer_top,
outbox, smtp_login, smtp_password, smtp_server, smtp_port, smtp_security_label, smtp_security, cert_check_label, cert_check, view_log_button" />
outbox, smtp_login, smtp_password, smtp_server, smtp_port, smtp_security_label, smtp_security, cert_check_label, cert_check, view_log_button, enforce_e2ee_switch" />
<ImageView
android:id="@+id/advanced_icon"
@@ -220,6 +220,23 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/imap_folder"
android:layout_width="0dp"
android:layout_height="58dp"
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
app:layout_constraintTop_toBottomOf="@id/imap_port">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/imap_folder_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:hint="IMAP folder" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/imap_security_label"
style="@style/TextAppearance.AppCompat.Caption"
@@ -228,7 +245,7 @@
android:text="@string/login_imap_security"
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
app:layout_constraintTop_toBottomOf="@id/imap_port" />
app:layout_constraintTop_toBottomOf="@id/imap_folder" />
<Spinner
android:id="@+id/imap_security"
@@ -362,6 +379,17 @@
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
app:layout_constraintTop_toBottomOf="@id/cert_check_label" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/enforce_e2ee_switch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@string/enforce_e2ee"
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
app:layout_constraintTop_toBottomOf="@id/cert_check" />
<TextView
android:id="@+id/view_log_button"
android:layout_width="0dp"
@@ -375,7 +403,7 @@
android:paddingBottom="32dp"
app:layout_constraintEnd_toEndOf="@id/guideline_root_end"
app:layout_constraintStart_toStartOf="@id/guideline_root_start"
app:layout_constraintTop_toBottomOf="@id/cert_check" />
app:layout_constraintTop_toBottomOf="@id/enforce_e2ee_switch" />
</androidx.constraintlayout.widget.ConstraintLayout>
+1 -1
View File
@@ -37,7 +37,7 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
+1 -1
View File
@@ -37,7 +37,7 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -36,20 +36,19 @@
android:layout_height="0dp"
android:layout_weight="1" />
<ViewStub
android:id="@+id/attachment_editor_stub"
android:inflatedId="@+id/attachment_editor"
android:layout="@layout/conversation_activity_attachment_editor_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView
android:id="@+id/conversation_activity_message_request_bottom_bar"
android:background="?android:attr/windowBackground"
android:background="@drawable/input_panel_rounded_bg"
android:elevation="1dp"
android:outlineProvider="background"
android:visibility="gone"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:paddingLeft="16dp"
android:paddingStart="16dp"
android:paddingRight="16dp"

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