Compare commits

..

222 Commits

Author SHA1 Message Date
adbenitez 64752d3bae Merge remote-tracking branch 'upstream/adb/chat-description' 2026-02-17 23:39:53 +01:00
adbenitez 6145e0d2df Merge remote-tracking branch 'upstream/main' 2026-02-17 23:29:06 +01:00
adbenitez b365284743 fix Rpc class 2026-02-17 22:37:56 +01:00
adb af62041c14 Merge branch 'main' into adb/chat-description 2026-02-17 22:36:07 +01:00
adbenitez 910dbf56fd update core 2026-02-17 22:34:55 +01:00
adb 4eca0dea4a Merge pull request #4237 from deltachat/prep-2.43.0
prepare 2.43.0
2026-02-17 22:24:24 +01:00
adbenitez a76c17fd45 prepare 2.43.0 2026-02-17 21:50:36 +01:00
adb b00aeec03a Merge pull request #4236 from deltachat/update-core-and-stuff-2026-02-17
Update core to 2.43.0
2026-02-17 21:15:35 +01:00
adbenitez ffe147fde8 update RPC bindings 2026-02-17 21:09:06 +01:00
adbenitez 02a5bb06a9 update translations 2026-02-17 21:08:44 +01:00
adbenitez a8997738bf update changelog 2026-02-17 20:07:51 +01:00
adbenitez 202690d02e update deltachat-core-rust to 'chore(release): prepare for 2.43.0' of 'v2.43.0' 2026-02-17 20:04:23 +01:00
wchen342 6d14bbdbc7 Merge pull request #4234 from deltachat/wch423/fix-audio-seekbar-reset
Prevent seek bar from being reset when message list changes
2026-02-17 14:03:08 -05:00
wch423 03a0b53eee Update function name 2026-02-17 16:34:33 +01:00
wch423 a044181a75 Update progress bar for playing audio; Clear progress for any other audio that is not playing to avoid confusion 2026-02-17 16:31:06 +01:00
B. Petersen e9ae9dc5bf add more translation context to strings 2026-02-17 15:27:13 +01:00
B. Petersen cbfa3b7e58 clarify 'Audio' string 2026-02-17 13:52:21 +01:00
adbenitez bf57d3bd73 add src/main/res/values-eu/strings_arcanechat.xml 2026-02-17 03:47:52 +01:00
adbenitez 6200f354ca set custom icon for debug build 2026-02-17 03:47:30 +01:00
wchen342 c8dfb08dcb Merge pull request #4228 from deltachat/wch423/fix-audio-duration
Add duration extraction for audio files
2026-02-16 16:03:06 -05:00
wch423 543e9d91e4 Make sure longs are always in range 2026-02-16 20:08:31 +01:00
wch423 dec5879919 Add duration extraction for audio files 2026-02-16 19:01:55 +01:00
wchen342 a1d21d8562 Merge pull request #4226 from deltachat/wch423/fix-video-crash
Fix video player crash
2026-02-16 04:55:34 -05:00
adbenitez 253ed877e5 add comment to src/main/res/values/strings_arcanechat.xml
hint to translators to point out that the channel is in English
2026-02-15 17:40:57 +01:00
wch423 b809d24ab1 Change PlayerView to StyledPlayerView to avoid conflict between ExoPlayer2 and Media3 2026-02-13 18:05:37 +01:00
adbenitez 78aee0a487 Merge remote-tracking branch 'upstream/main' 2026-02-13 02:19:21 +01:00
adb d16254c146 Merge pull request #4223 from deltachat/adb/issue-4208
remove old buttons in relays list
2026-02-12 20:10:01 +01:00
adb 7ab3ef8453 Merge pull request #4224 from deltachat/adb/issue-4207-part2
don't tint error icon red in chatlist and info-messages
2026-02-12 20:09:43 +01:00
adbenitez 4b695a3293 update changelog 2026-02-12 00:01:16 +01:00
adbenitez da4c382c9a add context menu to relay items 2026-02-11 23:58:48 +01:00
adbenitez 80973960f5 don't tint error icon red in chatlist and info-messages 2026-02-11 20:54:05 +01:00
adbenitez 7be75f7008 remove old buttons 2026-02-11 20:27:01 +01:00
adbenitez 6fa114c6c0 Merge remote-tracking branch 'upstream/main' 2026-02-11 20:22:37 +01:00
adbenitez 767b5f2bae update channel link 2026-02-11 20:04:59 +01:00
adb ed3a21b992 Merge pull request #4222 from deltachat/prep-2.42.0
prepare 2.42.0
2026-02-10 21:41:59 +01:00
adbenitez 899c2b5647 prepare 2.42.0 2026-02-10 21:24:21 +01:00
adb 61e616a53b Merge pull request #4221 from deltachat/update-core-and-stuff-2026-02-10
Update core to 2.42.0
2026-02-10 21:23:09 +01:00
adbenitez 3bf4504de9 update strings 2026-02-10 21:20:52 +01:00
adbenitez cd0740d895 update changelog 2026-02-10 20:31:58 +01:00
adbenitez 9f99edc159 update deltachat-core-rust to 'chore(release): prepare for 2.42.0' of 'v2.42.0' 2026-02-10 20:26:54 +01:00
wchen342 60568f23f5 Merge pull request #4217 from deltachat/wch423/onboarding-invite-code
Allow scanning invite codes from onboarding
2026-02-09 16:36:23 -05:00
wchen342 6dc8bd7ba8 Merge branch 'main' into wch423/onboarding-invite-code 2026-02-09 16:25:34 -05:00
wch423 ff4ecb3bda Fix missing callback on successful scan 2026-02-09 22:19:11 +01:00
wch423 c71e71359a Remove extra constant 2026-02-09 22:11:13 +01:00
wch423 d5a09fa25e Only show confirmation dialog if it is an invitation link for contact or group 2026-02-09 21:56:43 +01:00
adb f52c2be2c2 Merge pull request #4219 from deltachat/adb/disable-chat-edge-to-edge
disable insets change in setComposePanelVisibility()
2026-02-09 21:46:46 +01:00
wch423 2233b93108 Remove color change on the OK button 2026-02-09 21:42:06 +01:00
adbenitez d76eb3239a disable insets change in setComposePanelVisibility() 2026-02-09 21:05:00 +01:00
adb a982bd2bbb Merge branch 'main' into wch423/onboarding-invite-code 2026-02-09 18:52:33 +01:00
wchen342 9ef3edcc0e Merge pull request #4189 from deltachat/wch423/audio-background-play
Add background audio playback for voice messages
2026-02-09 12:45:01 -05:00
wch423 de7c54b886 Revert changes to rpc 2026-02-09 15:32:37 +01:00
adb 9c9f966597 Merge branch 'main' into wch423/audio-background-play 2026-02-09 02:24:54 +01:00
adb 12e77789d3 Merge branch 'main' into wch423/onboarding-invite-code 2026-02-09 02:24:39 +01:00
adb bd640072ab Merge pull request #4216 from deltachat/adb/avoid-races-loading-chatlist
use conversationListFragment.loadChatlistAsync() to avoid races
2026-02-09 01:00:38 +01:00
adb 0a9c46fbfc Merge branch 'main' into adb/avoid-races-loading-chatlist 2026-02-07 17:37:29 +01:00
wch423 1cc4f11484 Add CHANGELOG 2026-02-07 17:04:27 +01:00
wch423 9386f2f9cb Fix QR scanning not restarted after cancel 2026-02-07 16:56:49 +01:00
wchen342 45d73d4604 Merge pull request #4215 from deltachat/adb/issue-4207
tune down error icon, don't use special bright alarming color
2026-02-07 10:22:32 -05:00
Hocuri a5892330dd Translate stock strings 2026-02-07 11:30:40 +01:00
adbenitez 8a0e2d72a4 use conversationListFragment.loadChatlistAsync() to avoid races 2026-02-07 06:22:31 +01:00
adbenitez 2629c65564 tune down error icon, don't use special bright alarming color 2026-02-07 02:39:37 +01:00
adbenitez 10694a6809 display chat description in chat profile 2026-02-07 01:49:59 +01:00
adbenitez aa3d177243 allow to set chat description 2026-02-07 01:23:26 +01:00
adbenitez 4d35d4edeb temporarily select core branch with new chat description api 2026-02-06 23:08:53 +01:00
wch423 bbaba3cd33 Fix observer not added in a specific case 2026-02-06 22:30:35 +01:00
wch423 d1f002a132 Remove desugar and use AndroidX version of Consumer 2026-02-06 21:04:31 +01:00
wch423 9ce9a91c95 Add confirm dialog for invitation qr codes 2026-02-06 21:01:36 +01:00
wch423 7844e146b1 Join and launch chat directly after creating new account 2026-02-06 18:50:57 +01:00
adbenitez eb997eca00 fix bug introduced by upstream merge 2026-02-06 18:35:30 +01:00
adb 30124de2a8 Merge branch 'main' into wch423/audio-background-play 2026-02-06 18:34:56 +01:00
wch423 1dcf7e4860 Adding new text for scanning invite code when onboarding 2026-02-06 16:50:45 +01:00
B. Petersen 9ffc904ae5 more detailed call strings 2026-02-06 11:45:56 +01:00
wchen342 2704749b44 Merge pull request #4198 from deltachat/wch423/edge-to-edge-device-channel
Make device messages and subscribed channels edge to edge
2026-02-05 15:15:40 -05:00
wch423 1c174b5b70 Stop draft audio playback when (1) the conversation exits; or (2) the message is sent 2026-02-05 21:12:09 +01:00
adbenitez 8b5dd70d75 Merge remote-tracking branch 'upstream/main' 2026-02-05 20:25:35 +01:00
adb f022316ad7 Merge branch 'main' into wch423/audio-background-play 2026-02-05 19:44:50 +01:00
wch423 3386f5c5f7 Make !CanSend -> CanSend still trigger Inset changes 2026-02-05 19:07:43 +01:00
adb 57eead3a34 Merge branch 'main' into wch423/edge-to-edge-device-channel 2026-02-05 18:33:52 +01:00
adb 2868b51835 Merge pull request #4212 from deltachat/prep-2.40.0
prepare 2.40.0
2026-02-05 17:44:28 +01:00
wch423 82118db71b Make inset changes only happen on initialization of activity 2026-02-05 17:27:21 +01:00
adbenitez 14f55ca6b1 prepare 2.40.0 2026-02-05 17:25:20 +01:00
adb cebfa12142 Merge branch 'main' into wch423/edge-to-edge-device-channel 2026-02-05 16:55:30 +01:00
adb 96c8c21b78 Merge pull request #4209 from deltachat/adb/issue-4206
hide delete-contact icon in ContactSelectionListFragment
2026-02-05 16:55:07 +01:00
adb 9ab1b1f3a7 Merge branch 'main' into adb/issue-4206 2026-02-05 16:54:55 +01:00
adb 94a0e426f8 Merge pull request #4210 from deltachat/adb/allow-to-pick-call-mode
show menu to select audio/video call modes
2026-02-05 16:48:27 +01:00
adbenitez 006f8ae826 use hasVideo instead of audioOnly 2026-02-05 16:37:42 +01:00
wch423 2889266522 Force dispatch all inset changes immediately in ConversationFragment 2026-02-05 16:28:38 +01:00
adb 5e6fccf143 Merge branch 'main' into wch423/edge-to-edge-device-channel 2026-02-05 15:41:17 +01:00
adbenitez 3847e20d18 set tint for icons in the call submenu 2026-02-05 02:31:38 +01:00
adbenitez fc69212a51 update changelog 2026-02-05 01:41:29 +01:00
adbenitez 8999f54ba2 upgrade calls web app 2026-02-05 01:40:27 +01:00
adbenitez f470e92300 show different icon depending on voice/video call modes 2026-02-05 01:35:20 +01:00
adbenitez 3ac49e3e58 set the call mode when calling Rpc.placeOutgoingCall 2026-02-05 01:20:11 +01:00
adb 8dd9cfec5b Merge branch 'main' into adb/allow-to-pick-call-mode 2026-02-05 01:17:12 +01:00
adb 0588214ee7 Merge pull request #4211 from deltachat/update-core-and-stuff-2026-02-04
Update core to 2.40.0
2026-02-05 00:37:53 +01:00
adbenitez 4ec49a031c adapt to new Rpc.placeOutgoingCall API 2026-02-05 00:24:51 +01:00
adbenitez a31d7d6d3e update the RPC bindings 2026-02-04 23:39:04 +01:00
adbenitez 64bbe9866d update translations 2026-02-04 23:38:33 +01:00
adbenitez 5d7ab84efc update changelog 2026-02-04 23:35:14 +01:00
adbenitez 11f73a88e8 update deltachat-core-rust to 'chore(release): prepare for 2.40.0' of 'v2.40.0' 2026-02-04 23:31:30 +01:00
adbenitez dd0e847976 take into consideration if the call is audio/video call when accepting it 2026-02-04 23:17:42 +01:00
wch423 71ed333468 Stop playback when attachment preview is removed;
Stop playback when message containing audio is removed
2026-02-04 22:49:18 +01:00
adbenitez 41d94ae3ee if it is an audio-only call start with video disabled 2026-02-04 22:14:50 +01:00
wch423 b66bc1f863 Handle specific case when leaving group; also move floating button to avoid overaapping with system bar 2026-02-04 19:36:51 +01:00
adbenitez df7d80319c show menu to select audio/video call modes 2026-02-04 19:36:34 +01:00
adbenitez 134145d166 update changelog 2026-02-04 17:13:48 +01:00
adbenitez 688a103c10 hide delete-contact icon in ContactSelectionListFragment 2026-02-04 16:45:29 +01:00
adb e2efa1f913 Merge pull request #4201 from deltachat/adb/issue-4195
fix mailto handling and remove unused code
2026-02-03 23:27:48 +01:00
adb aebd5c66f7 Merge branch 'main' into wch423/audio-background-play 2026-02-03 21:51:02 +01:00
adbenitez 15c60c6b12 fix mailto handling 2026-02-03 20:57:47 +01:00
wch423 f319ba2b83 Allow list scrolling to extend to edge 2026-02-03 19:40:02 +01:00
wch423 93f12e7367 Make message list respect bottom bar height 2026-02-03 19:40:00 +01:00
wch423 10acb07f82 Make device messages and subscribed channels edge to edge 2026-02-03 19:39:56 +01:00
adb ee9a8dd53a Merge branch 'main' into adb/issue-4195 2026-02-03 18:22:39 +01:00
adb 0a55023bdb Merge pull request #4205 from deltachat/adb/issue-4202
remove deselected members
2026-02-03 18:22:02 +01:00
adb fec9f8b4d3 Merge pull request #4204 from deltachat/adb/issue-4203
Fix layout problems inside in-chat apps
2026-02-03 17:53:22 +01:00
wch423 13374df709 Bug fixes and minor changes 2026-02-03 17:41:31 +01:00
adbenitez 7fa04dc3c0 update changelog 2026-02-03 01:03:05 +01:00
adbenitez 9d4e0e4e21 remove deselected members 2026-02-03 00:40:00 +01:00
adbenitez 473c28ab07 Fix layout problems inside in-chat apps 2026-02-02 20:31:42 +01:00
adbenitez de47feac40 update changelog 2026-01-30 20:21:48 +01:00
adbenitez 84894ff538 fix mailto handling and remove unused code 2026-01-30 20:17:04 +01:00
adb f254c35749 Merge pull request #4199 from deltachat/r10s/improve-details-strings
message info: title-case the send/received titles
2026-01-30 17:48:37 +01:00
adb 1ff4e069ea Merge branch 'main' into wch423/audio-background-play 2026-01-29 22:17:24 +01:00
B. Petersen d574d33596 title-case the message-info send/received titles
we nowhere in the app use lower case field names
2026-01-29 18:33:57 +01:00
wchen342 f6756fc34b Merge pull request #4191 from deltachat/adb/issue-4157
use event emmiter before DcAccounts creation
2026-01-29 10:34:57 -05:00
wchen342 7c27eb47fc Merge pull request #4196 from deltachat/adb/issue-4194
avoid crash caused by custom Fragment constructors
2026-01-29 10:19:15 -05:00
adbenitez 8d5a55c24c update changelog 2026-01-29 03:47:53 +01:00
adbenitez 2e9aa79b02 simplify changes in AccountSelectionListFragment 2026-01-29 03:44:42 +01:00
adbenitez 1f9264225b remove custom constructor in ReactionsDetailsFragment 2026-01-29 03:30:17 +01:00
adbenitez 18e145faaf avoid custom constructor in AccountSelectionListFragment 2026-01-29 02:17:06 +01:00
adbenitez 47cf70120a add logcat to crash report 2026-01-28 23:21:37 +01:00
adbenitez 86451a00ff Merge remote-tracking branch 'upstream/main' 2026-01-28 22:28:40 +01:00
wch423 34be7aab17 Cleanup imports 2026-01-28 21:30:51 +01:00
wch423 59139ed242 Add support for attachment draft; Distinguish between different messages; Prevent unrelated activities from changing the pending intent 2026-01-28 21:28:43 +01:00
wch423 11f3964bdc Add support for attachment draft; Distinguish between different messages 2026-01-28 21:28:42 +01:00
wch423 7683408d18 Allow going back to previous activity from the notification; Support AllMediaActivity 2026-01-28 21:28:40 +01:00
wch423 b74e793654 Finish ViewModel for audio playback; Refine UI set up for AudioView 2026-01-28 21:28:38 +01:00
wch423 4bd74324d2 Try to decouple view and business logics 2026-01-28 21:28:36 +01:00
wch423 9a121b3039 Fix UI update problems;
Make notification clickable
2026-01-28 21:28:33 +01:00
wch423 a9832c9c53 Make audio view use Media3 for playback 2026-01-28 21:28:31 +01:00
adbenitez 1e4c8bc291 use event emmiter before DcAccounts creation 2026-01-27 19:05:38 +01:00
wchen342 c45a47e53c Merge pull request #4187 from deltachat/adb/issue-4176
show views count in channels (to the admin)
2026-01-27 09:35:25 -05:00
adb a07723d515 Merge branch 'main' into adb/issue-4176 2026-01-27 03:02:39 +01:00
adb 02d7a6fb1f Merge pull request #4190 from deltachat/adb/crash-sooner-in-DcAccounts
throw exception if DcAccounts.createAccountsCPtr() returned null pointer
2026-01-27 03:02:15 +01:00
adb d62e8fa864 Merge branch 'main' into adb/crash-sooner-in-DcAccounts 2026-01-27 01:25:30 +01:00
adbenitez a03fd337ef throw exception if createAccountsCPtr() returned null pointer 2026-01-27 01:20:27 +01:00
72374 27c0f45430 fix: Change maximum height and width of avatar-images to the maximum allowed by Chatmail Core (#4185)
Avatar-images with a resolution larger than 512x512 will be resized by Chatmail Core, while media-quality is set to balanced, which is currently the highest quality-setting.

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-01-26 23:27:56 +00:00
adbenitez a0f1232261 update changelog 2026-01-26 22:30:57 +01:00
adbenitez 5640d31080 remove checkmark from chatlist for outgoing channel messages 2026-01-26 22:29:03 +01:00
adbenitez 1542fc67ca show number of views a channel message have 2026-01-26 21:31:41 +01:00
adbenitez a99a4ec85b hide delivery checkmark in channels 2026-01-26 18:23:55 +01:00
adb fa17b134cf Merge pull request #4180 from deltachat/adb/issue-4055
allow to switch profile while sharing/forwarding
2026-01-26 17:05:49 +01:00
adbenitez 6898a37704 fix WelcomeActivity 2026-01-24 02:44:18 +01:00
adbenitez 45b6a5c565 Merge remote-tracking branch 'upstream/main' 2026-01-24 02:44:04 +01:00
adb 7330e6ba39 Merge branch 'main' into adb/issue-4055 2026-01-24 00:57:40 +01:00
adb 8f405914a2 Merge pull request #4186 from deltachat/update-core-2.39.0
update core to 2.39.0
2026-01-24 00:57:09 +01:00
adbenitez 6d09a6f318 update changelog 2026-01-24 00:34:18 +01:00
adbenitez b349a7e601 update deltachat-core-rust to 'chore(release): prepare for 2.39.0' of 'v2.39.0' 2026-01-24 00:31:58 +01:00
adbenitez 0d610895d2 fix function 2026-01-23 22:13:59 +01:00
adbenitez a65c677ecc update changelog 2026-01-23 22:10:06 +01:00
adbenitez db93aeacc0 fix event listener in ConversationActivity 2026-01-23 22:05:41 +01:00
adb 09ff0d888b Merge branch 'main' into adb/issue-4055 2026-01-23 17:43:51 +01:00
adb b9c5e741e8 Merge pull request #4184 from deltachat/update-core-and-stuff-2026-01-23
Update core to 3.38.0
2026-01-23 17:43:28 +01:00
adbenitez 620d86c903 update RPC bindings 2026-01-23 17:41:25 +01:00
adbenitez 14e31650e7 update translations 2026-01-23 17:35:19 +01:00
adbenitez 9848cb5847 update changelog 2026-01-23 17:27:49 +01:00
adbenitez bf7db005ef update deltachat-core-rust to 'chore(release): prepare for 2.38.0' of 'v2.38.0' 2026-01-23 15:31:28 +01:00
adbenitez aa032dbb37 fix some bugs
* avoid using wrong DcContext after switching account in ConversationListActivity
* avoid getContext() in ConversationFragment, use Rpc instead
2026-01-22 20:45:33 +01:00
adbenitez 9e1b9775e2 fix ConversationListActivity 2026-01-22 18:43:54 +01:00
adbenitez fcd16d9e32 don't allow to delete or create new profile while sharing/forwarding 2026-01-22 17:50:16 +01:00
adbenitez 2a4bd2a001 add device messages on account switching 2026-01-22 17:31:32 +01:00
adbenitez eacd089c8f improve code in ConversationListActivity 2026-01-22 17:18:49 +01:00
adbenitez 9ed355efd1 don't save dcContext in MessageSelectorFragment and derivates 2026-01-21 20:59:39 +01:00
adbenitez d1bb25004d Merge branch 'adb/issue-4055' of https://github.com/deltachat/deltachat-android into adb/issue-4055 2026-01-21 20:21:28 +01:00
adbenitez 8d1b010d1b tweak SendRelayedMessageUtil 2026-01-21 20:21:10 +01:00
adb 8a9b76af3a Merge branch 'main' into adb/issue-4055 2026-01-21 20:13:24 +01:00
adb 4bbd9428e3 Merge pull request #4182 from deltachat/wch423/backpress-migration
Migrate deprecated `onBackPressed` calls
2026-01-21 20:05:14 +01:00
adb 83646003b6 Merge branch 'main' into adb/issue-4055 2026-01-21 19:45:10 +01:00
wch423 947887f335 Migrate deprecated onBackPressed calls 2026-01-21 18:59:56 +01:00
wchen342 69631cd857 Merge pull request #4181 from deltachat/adb/improve-InstantOnboardingActivity
avoid account rollback in InstantOnboardingActivity
2026-01-21 10:30:01 -05:00
adbenitez 0722426425 allow to forward messages to different account 2026-01-21 00:22:43 +01:00
adbenitez 98d54bbe6a prevent returning to old chat after switching account 2026-01-21 00:20:02 +01:00
adbenitez c1dc0cf127 ConversationActivity: use currently selected account
instead of storing dcContext once on creation, the selected account
might change
2026-01-21 00:14:41 +01:00
adbenitez 44a1ce42f0 avoid account rollback in InstantOnboardingActivity 2026-01-20 20:01:40 +01:00
adbenitez a0c6cef0fa add menu item to switch account 2026-01-20 19:44:32 +01:00
adbenitez f96e89956c account switcher: make switching accounts smoother 2026-01-20 16:31:02 +01:00
wchen342 ce9ded9d8b Merge pull request #4178 from deltachat/adb/issue-4174
Properly update menu and other actionMode state in ContactSelectionListFragment
2026-01-20 10:10:45 -05:00
adb cb2d9f16ac Merge branch 'main' into adb/issue-4174 2026-01-20 15:47:44 +01:00
wchen342 c2a13ae9dd Merge pull request #4179 from deltachat/adb/issue-4175
avoid empty profiles after using "add as second device" from welcome screen
2026-01-20 09:35:51 -05:00
adbenitez f8c2403e61 update changelog 2026-01-19 22:26:35 +01:00
adbenitez 616b495426 only create a new account if the selected one is already configured 2026-01-19 22:13:47 +01:00
adbenitez a22baa7fc3 update changelog 2026-01-19 19:29:12 +01:00
adbenitez 0b9bb9ff76 fix: properly update menu and other actionMode state in ContactSelectionListFragment 2026-01-19 19:18:41 +01:00
adb ef96192d8b Merge pull request #4173 from deltachat/wch423/fix-resume-wrong-activity
Do not launch WelcomeActivity as singleTask
2026-01-19 18:32:00 +01:00
B. Petersen 13c34d9480 make clear, the blockage comes from user action 2026-01-19 17:47:48 +01:00
adb 610f4cebc2 Merge pull request #4171 from deltachat/wch423/add-hint-empty-group-search
Show hint during Contact search if results are empty
2026-01-19 17:45:18 +01:00
adbenitez d4d15eee7c avoid showing "no results for null" if search term is empty 2026-01-19 17:39:26 +01:00
wch423 ee3886285b Show "No results found for" during Contact search if results are empty 2026-01-19 15:46:00 +01:00
wchen342 a32c20473f Merge pull request #4172 from deltachat/wch423/blocked-contact-profile-indicator
Add indication for blocked contacts in user profile
2026-01-19 08:19:25 -05:00
wch423 db956d93cf Do not launch WelcomeActivity as singleTask 2026-01-16 19:50:29 +01:00
wch423 6a5a75131e Add indication for blocked contacts in user profile 2026-01-16 17:44:34 +01:00
wchen342 a676a47ed4 Merge pull request #4170 from deltachat/adb/issue-3913
refresh chatlist when going back from chat
2026-01-16 13:51:04 +01:00
adbenitez 2067122d2c update changelog 2026-01-15 23:41:41 +01:00
adbenitez 8a0fe892f4 refresh chatlist when going back from chat 2026-01-15 23:33:57 +01:00
adb 29a6f4eb31 Merge pull request #4167 from deltachat/wch423/mark-external-links
Mark external links with a special character
2026-01-15 19:39:51 +01:00
adb f610592bff Merge branch 'main' into wch423/mark-external-links 2026-01-15 19:20:37 +01:00
adb 898008e2a1 Merge pull request #4168 from deltachat/wch423/larger-qr-code
Use new generation method so the QR code is larger
2026-01-15 19:20:01 +01:00
adb 9eaa3ca182 Merge branch 'main' into wch423/larger-qr-code 2026-01-15 18:59:13 +01:00
adb c98818e5cf Merge pull request #4166 from deltachat/wch423/notification-permission-second-device
Ask for Notification Permission when adding second device
2026-01-15 18:57:49 +01:00
wch423 7643ecd90a Use new generation method so the QR code is larger 2026-01-15 18:25:56 +01:00
wch423 e4993bd302 Add CHANGELOG 2026-01-15 16:58:52 +01:00
wch423 32ee198128 Add util methods to handle RTL/LTR text concatenations;
Add markers to external links
2026-01-15 16:56:29 +01:00
wch423 ab0d33b3c0 Ask for Notification Permission when adding second device 2026-01-15 14:07:58 +01:00
adb 2c8b781817 Merge pull request #4165 from deltachat/wch423/fix-qr-scanner-no-error-dialog
Fix dialogs not showing when pasted QR codes are not valid
2026-01-14 21:31:59 +01:00
wch423 e59655c456 Fix dialogs not showing when pasted QR codes are not valid 2026-01-14 21:21:17 +01:00
adb 2ce7e5a3c7 Merge pull request #4164 from deltachat/wch423/remove-profile-email
Remove email address from profile
2026-01-14 16:28:25 +01:00
wch423 79c7419b71 Update CHANGELOG.md 2026-01-14 16:16:55 +01:00
wch423 2efaf1df93 Remove email address from profile 2026-01-14 15:59:24 +01:00
adb 814ac7d976 Merge pull request #4162 from deltachat/adb/remove-from-welcome-in-CreateProfileActivity
remove FROM_WELCOME in CreateProfileActivity
2026-01-13 20:40:18 +01:00
adbenitez b0e5c1c634 remove FROM_WELCOME in CreateProfileActivity 2026-01-13 20:23:41 +01:00
B. Petersen bae998f811 truncate file names in the middle
that way, the end is always visible,
which contains important information more often than the middle.

this is common knowledge since the 90's or so
however, as quite some previously stable ux patterns, lost on the way.
(cmp. Scott Jensen, https://www.youtube.com/watch?v=1fZTOjd_bOQ )
2026-01-08 17:54:50 +01:00
169 changed files with 5225 additions and 2824 deletions
+28 -2
View File
@@ -1,10 +1,36 @@
# Delta Chat Android Changelog
## Unreleased
## v2.43.0
2026-02
* Improve switch speed when changing profiles
* Allow to switch profile when sharing or forwarding
* Display message views count for channel owners
* Don't notify notification-to-all from in-chat apps if the chat is muted
* Allow to see inbox quota for all relays in connectivity screen
* Update to core 2.36.0
* Truncate file names in the middle, not at the end; important information are more often at the end
* Remove email address from user profile
* Mark external links with " ↗" to make them clear
* Make QR code larger on "Add Second Device" screen
* Add indication for blocked contacts in user profile
* Allow to start calls with video disabled
* Show hint for empty contact search results
* Add background playing for voice messages and other audio files
* Allow scanning Invitation Code when creating a new profile
* Add context menu in long-pressing relays items instead of showing buttons
* Enhanced video player UI
* Fix: Show dialog if pasted QR codes are invalid
* Fix: Refresh chat list when returning from conversation if selected profile changed
* Fix: Update menu when using "select all" in contact selection
* Fix: Avoid empty profiles after using "add as second device" from welcome screen
* Fix: Remove from group deselected members in the contact selection list
* Fix multi-device seen messages synchronization when using multiple relays
* Fix mailto handling
* Fix layout problems inside in-chat apps
* Fix real-time for in-chat apps that need it
* Avoid crash when the app is minimized with profile switcher or reactions dialogs open
* Remove "trash icon" option from contact selection list when adding members to group
* Update to core 2.43.0
## v2.35.0
2026-01
+8 -3
View File
@@ -33,8 +33,8 @@ android {
useLibrary 'org.apache.http.legacy'
defaultConfig {
versionCode 30000736
versionName "2.36.0"
versionCode 30000737
versionName "2.43.0"
applicationId "chat.delta.lite"
multiDexEnabled true
@@ -211,6 +211,8 @@ dependencies {
implementation "io.noties.markwon:inline-parser:$markwon_version"
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
implementation 'androidx.concurrent:concurrent-futures:1.3.0'
implementation 'androidx.sharetarget:sharetarget:1.2.0'
implementation 'androidx.webkit:webkit:1.14.0'
@@ -231,8 +233,11 @@ dependencies {
implementation 'androidx.work:work-runtime:2.9.1'
implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
implementation 'com.google.guava:guava:31.1-android'
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // FIXME: exoplayer dependencies kept for Video, but we shall migrate them at some point
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation "androidx.media3:media3-session:$media3_version"
implementation "androidx.media3:media3-ui:$media3_version"
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24
implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner
+39 -3
View File
@@ -153,6 +153,42 @@ static uint32_t* jintArray2uint32Pointer(JNIEnv* env, jintArray ja, int* ret_icn
}
/************************************************************
* DcEventChannel
************************************************************/
static dc_event_channel_t* get_dc_event_channel(JNIEnv *env, jobject obj)
{
static jfieldID fid = 0;
if (fid==0) {
jclass cls = (*env)->GetObjectClass(env, obj);
fid = (*env)->GetFieldID(env, cls, "eventChannelCPtr", "J" /*Signature, J=long*/);
}
if (fid) {
return (dc_event_channel_t*)(*env)->GetLongField(env, obj, fid);
}
return NULL;
}
JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_createEventChannelCPtr(JNIEnv *env, jobject obj)
{
return (jlong)dc_event_channel_new();
}
JNIEXPORT void Java_com_b44t_messenger_DcEventChannel_unrefEventChannelCPtr(JNIEnv *env, jobject obj)
{
dc_event_channel_unref(get_dc_event_channel(env, obj));
}
JNIEXPORT jlong Java_com_b44t_messenger_DcEventChannel_getEventEmitterCPtr(JNIEnv *env, jobject obj)
{
return (jlong)dc_event_channel_get_event_emitter(get_dc_event_channel(env, obj));
}
/*******************************************************************************
* DcAccounts
******************************************************************************/
@@ -172,11 +208,11 @@ static dc_accounts_t* get_dc_accounts(JNIEnv *env, jobject obj)
}
JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir)
JNIEXPORT jlong Java_com_b44t_messenger_DcAccounts_createAccountsCPtr(JNIEnv *env, jobject obj, jstring dir, jobject chanObj)
{
CHAR_REF(dir);
int writable = 1;
jlong accountsCPtr = (jlong)dc_accounts_new(dirPtr, writable);
jlong accountsCPtr = (jlong)dc_accounts_new_with_event_channel(dirPtr, writable, get_dc_event_channel(env, chanObj));
CHAR_UNREF(dir);
return accountsCPtr;
}
@@ -1957,7 +1993,7 @@ JNIEXPORT jstring Java_com_b44t_messenger_DcBackupProvider_getQr(JNIEnv *env, jo
JNIEXPORT jstring Java_com_b44t_messenger_DcBackupProvider_getQrSvg(JNIEnv *env, jobject obj)
{
char* temp = dc_backup_provider_get_qr_svg(get_dc_backup_provider(env, obj));
char* temp = dc_create_qr_svg(dc_backup_provider_get_qr(get_dc_backup_provider(env, obj)));
jstring ret = JSTRING_NEW(temp);
dc_str_unref(temp);
return ret;
@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="228"
android:viewportHeight="280">
<group android:scaleX="0.35014287"
android:scaleY="0.43"
android:translateX="74.08372"
android:translateY="79.8">
<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="@color/ic_launcher_background"/>
<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="@color/ic_launcher_background"/>
</group>
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
+12 -2
View File
@@ -38,6 +38,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
@@ -54,7 +55,6 @@
android:requestLegacyExternalStorage="true"
tools:targetApi="TIRAMISU"
android:hasFragileUserData="true"
android:enableOnBackInvokedCallback="false"
>
<!-- android car support, see https://developer.android.com/training/auto/start/,
@@ -93,6 +93,7 @@
<intent-filter>
<data android:scheme="mailto"/>
<action android:name="android.intent.action.SENDTO"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
@@ -220,7 +221,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".WelcomeActivity"
android:launchMode="singleTask"
android:launchMode="singleTop"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -391,6 +392,15 @@
android:name=".service.FetchForegroundService"
android:foregroundServiceType="dataSync" />
<service
android:name=".service.AudioPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
<receiver android:name=".notifications.MarkReadReceiver"
android:enabled="true"
android:exported="false">
File diff suppressed because one or more lines are too long
+38 -52
View File
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1663,52 +1663,38 @@ ordered chronologically:</p>
<ul>
<li>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Sometimes we receive one-time donations from private individuals.
+72 -83
View File
@@ -1045,31 +1045,31 @@ für die <a href="https://chatmail.at/clients">Chatmail-Clients</a> umfasst, von
</h3>
<p>We would like to improve Delta Chat with your help,
which is why Delta Chat for Android asks whether you want
to send anonymous usage statistics.</p>
<p>Wir möchten Delta Chat mit deiner Hilfe verbessern.
Deshalb fragt Delta Chat für Android, ob du
anonyme Nutzungsstatistiken senden möchtest.</p>
<p>You can turn it on and off at
<strong>Settings → Advanced → Send statistics to Delta Chats developers</strong>.</p>
<p>Du kannst dies unter
<strong>Einstellungen → Erweitert → Statistik an Delta Chat Entwickler senden</strong> ein- und ausschalten.</p>
<p>When you turn it on,
weekly statistics will be automatically sent to a bot.</p>
<p>Wenn eingeschaltet,
werden wöchentlich Statistiken automatisch an einen Bot gesendet.</p>
<p>We are interested e.g. in statistics like:</p>
<p>Wir sind beispielsweise an folgenden Statistiken interessiert:</p>
<ul>
<li>
<p>How many contacts are introduced by personally scanning a QR code?</p>
<p>Wie viele Kontakte werden durch das persönliche Scannen eines QR-Codes hergestellt?</p>
</li>
<li>
<p>Which versions of Delta Chat are being used?</p>
<p>Welche Versionen von Delta Chat werden verwendet?</p>
</li>
<li>
<p>What errors occur for users?</p>
<p>Welche Fehler treten bei Benutzern auf?</p>
</li>
</ul>
<p>We will <em>not</em> collect any personally identifiable information about you.</p>
<p>Wir werden <em>keinerlei</em> personenbezogene Daten über dich sammeln.</p>
<h3 id="ich-bin-an-technischen-details-interessiert-gibt-es-hierzu-weitere-infos">
@@ -1107,6 +1107,10 @@ zum Austausch von Verschlüsselungsinformationen durch Scannen von QR-Codes oder
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> wird verwendet, um automatisch eine Ende-zu-Ende-Verschlüsselung zwischen Kontakten und allen Mitgliedern einer Gruppe herzustellen.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
wir post-quantum-resistente Verschlüsselung und Forward Secrecy einführen.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Teilen eines Kontakts im Chat</a>
ermöglicht es den Empfängern, eine Ende-zu-Ende-Verschlüsselung mit dem Kontakt zu verwenden.</p>
@@ -1280,15 +1284,13 @@ selbst wenn die Nachricht nicht Ende-zu-Ende-verschlüsselt war.</p>
speichern Delta-Chat-Apps keine Metadaten über Kontakte oder Gruppen auf Servern. Auch nicht in verschlüsselter Form.
Stattdessen werden alle Gruppen-Metadaten durchgängig verschlüsselt und ausschließlich auf den Endgeräten der Nutzer gespeichert.</p>
<p>Servers can therefore only see:</p>
<p>Server können daher nur das folgende sehen:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Absender- und Empfängeradressen, standardmäßig zufällig generiert</li>
<li>Größe der Nachricht</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>Alle anderen Metadaten zu Nachrichten, Kontakten und Gruppen befinden sich im Ende-zu-Ende-verschlüsselten Teil der Nachrichten.</p>
<h3 id="device-seizure">
@@ -1299,15 +1301,15 @@ Stattdessen werden alle Gruppen-Metadaten durchgängig verschlüsselt und aussch
</h3>
<p>Both for protecting against metadata-collecting servers
as well as against the threat of device seizure
we recommend to use a <a href="https://chatmail.at/relays">chatmail relay</a>
to create chat profiles using random addresses for transport.
Note that Delta Chat apps on all platforms support multiple profiles
so you can easily use situation-specific profiles next to your “main” profile
with the knowledge that all their data, along with all metadata, will be deleted.
Moreover, if a device is seized then chat contacts using short-lived profiles
can not be identified easily.</p>
<p>Sowohl zum Schutz vor Servern, die Metadaten sammeln,
als auch als Schutz bei Beschlagnahmung von Geräten
empfehlen wir die Verwendung eines <a href="https://chatmail.at/relays">Chatmail-Relays</a>,
um Chat-Profile mit zufälligen Adressen für den Transport zu erstellen.
Beachte, dass Delta-Chat-Apps mehrere Profile unterstützen,
sodass du neben deinem „Hauptprofil” ganz einfach situationsspezifische Profile verwenden kannst,
mit der Gewissheit, dass alle Daten sowie alle Metadaten gelöscht werden.
Darüber hinaus können Chat-Kontakte, die kurzlebige Profile verwenden,
im Falle einer Beschlagnahmung des Geräts nicht ohne Weiteres identifiziert werden.</p>
<h3 id="wer-sieht-meine-ip-adresse">
@@ -1317,7 +1319,7 @@ can not be identified easily.</p>
</h3>
<p>Das verwendete <a href="#relays">Rekay</a> muss Ihre IP-Adresse kennen,
<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>
@@ -1350,11 +1352,10 @@ um seine Serverinfrastruktur darüber im Unklaren zu lassen, wer eine Nachricht
Dies ist besonders wichtig, weil der Signal-Server die Handynummer jedes Kontos kennt,
die in der Regel mit einer Passidentität verbunden ist.</p>
<p>Even if <a href="https://chatmail.at/relays">chatmail relays</a>
do not ask for any private data (including no phone numbers),
it might still be worthwhile to protect relational metadata between addresses.
We dont foresee bigger problems in using random throw-away addresses for sealed sending
but an implementation has not been agreed as a priority yet.</p>
<p>Auch wenn <a href="https://chatmail.at/relays">Chatmail-Relays</a>
keine privaten Daten (einschließlich Telefonnummern) abfragen,
könnte es dennoch sinnvoll sein, Metadaten zwischen Adressen zu schützen.
Wir sehen keine größeren Probleme bei der Verwendung von zufälligen Wegwerfadressen für aber eine Umsetzung wurde noch nicht als priorisiert.</p>
<h3 id="pfs">
@@ -1364,23 +1365,20 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Nein, noch nicht.</p>
<p>Noch nicht, aber es kommt mit <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
and someone has collected your prior in-transit messages,
they will be able to decrypt and read them using the leaked decryption key.
Note that Forward Secrecy only increases security if you delete messages.
Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>Delta Chat unterstützt derzeit keine Perfect Forward Secrecy (PFS).
Das bedeutet, dass, wenn Ihr privater Schlüssel offengelegt wird
und jemand Ihre früheren Nachrichten während der Übertragung gesammelt hat,
diese mit dem offengelegten Schlüssel entschlüsselt und gelesen werden können.
Beachten Sie, dass Forward Secrecy die Sicherheit nur erhöht, wenn du Nachrichten löschst.
Andernfalls kann jemand, der deinen Schlüssel erhält,
in der Regel auch alle deine nicht gelöschten Nachrichten abrufen
und muss zuvor gesammelte Nachrichten nicht einmal entschlüsseln.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
wird durch automatische Schlüsselrotation eine zuverlässige Löschung (Forward Secrecy) gewährleisten.
Dieser Ansatz ist im Entwurf <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> festgelegt.</p>
<h3 id="pqc">
@@ -1390,11 +1388,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>Nein, noch nicht.</p>
<p>Noch nicht, aber es kommt mit <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat verwendet die Rust OpenPGP-Bibliothek <a href="https://github.com/rpgp/rpgp">rPGP</a>
die den neuesten <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP Entwurf</a> unterstützt.
Wir beabsichtigen, PQC-Unterstützung zum <a href="https://github.com/chatmail/core">chatmail core</a> hinzuzufügen, sobald der Entwurf bei der IETF in Zusammenarbeit mit anderen OpenPGP-Implementierern fertiggestellt ist.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, dessen vollständige Implementierung für 2026 geplant ist,
wird eine post-quantum-resistente Verschlüsselung zum Schutz vor Angriffen durch Quantencomputer bieten.
Delta Chat verwendet die Rust-OpenPGP-Bibliothek <a href="https://github.com/rpgp/rpgp">rPGP</a>,
die den neuesten <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Kryptografie OpenPGP Entfurf</a> unterstützt.
Die Implementierung ist in <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> festgelegt.</p>
<h3 id="wie-kann-ich-die-verschlüsselung-manuell-überprüfen">
@@ -1420,12 +1420,11 @@ ist die Verbindung sicher.</p>
<p>Nein.</p>
<p>Delta Chat generates secure OpenPGP keys according to the Autocrypt specification 1.1.
We do not recommend or offer users to perform manual key management.
We want to ensure that security audits can focus on a few proven cryptographic algorithms
instead of the full breadth of possible algorithms allowed with OpenPGP.
If you want to extract your OpenPGP key, there only is an expert method:
you need to look it up in the “keypairs” SQLite table of a profile backup tar-file.</p>
<p>Delta Chat generiert sichere OpenPGP-Schlüssel gemäß der Autocrypt-Spezifikation 1.1.
Wir bieten Benutzern keine manuelle Schlüsselverwaltung an, noch empfehlen diese.
Wir wollen sicherstellen, dass sich Sicherheitsüberprüfungen auf einige wenige bewährte kryptografische Algorithmen konzentrieren können,
anstatt auf die gesamte Bandbreite der mit OpenPGP zulässigen Algorithmen.
Wenn Sie Ihren OpenPGP-Schlüssel extrahieren möchten, gibt es nur eine Methode für Experten: Sie müssen ihn in der SQLite-Tabelle „keypairs” des Backups nachschlagen.</p>
<h3 id="security-audits">
@@ -1487,10 +1486,10 @@ Es wurden keine kritischen Probleme gefunden, aber zwei Probleme mit hohem Schwe
</h3>
<p>Some features require certain permissions,
e.g. you need to grant camera permission if you want to <a href="#howtoe2ee">scan an invite QR code</a>.</p>
<p>Einige Features erfordern bestimmte Berechtigungen.
So muss z.B. der Kamerazugriff gewährt werden, wenn du einen <a href="#howtoe2ee">QR-Code scannen</a> möchtest.</p>
<p>See <a href="https://delta.chat/en/gdpr#24-app-permissions">Privacy Policy</a> for a detailed overview.</p>
<p>Siehe <a href="https://delta.chat/de/gdpr#24-berechtigungen-der-app">Datenschutzhinweise</a> für eine detaillierte Übersicht.</p>
<h3 id="wo-können-meine-freunde-delta-chat-finden">
@@ -1534,29 +1533,6 @@ Wir nutzen vielmehr öffentliche Finanzierungsquellen, die bisher aus der EU und
<p>Konkret wurden die Delta-Chat-Entwicklungen bisher aus diesen Quellen finanziert:</p>
<ul>
<li>
<p>Das EU-Projekt <a href="https://nextleap.eu">NEXTLEAP</a> finanzierte 2017 und 2018 die Entwicklung und Implementierung von “Verifizierten Gruppen” und “Setup Kontakt” und half auch bei der Integration der Ende-zu-Ende-Verschlüsselung durch <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Der <a href="https://opentechfund.org">Open Technology Fund</a> hat Delta Chat erstmals 2018/2019 bezuschusst; mit dieser Förderung (~$200K) wurden hauptsächlich die Android-App verbessert sowie das Release der Desktop-App in einer Betaversion ermöglicht. Basierend auf Nutzererfahrungen im Menschenrechtskontext wurden zudem verschiedene Funktionen entwickelt, siehe unseren Bericht <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
Die zweite Förderung 2019/2020 (~$300K) half uns bei der Erstellung der iOS-Version, unsere Kernbibliothek in die Programmiersprache “Rust” zu konvertieren und neue Funktionen für alle Plattformen bereitzustellen.</p>
</li>
<li>
<p>Die <a href="https://nlnet.nl/">NLnet-Stiftung</a> bewilligte 2019/2020 46K EUR für die Fertigstellung von Rust-/Python-Bindungs und die Einrichtung eines Chat-Bot-Ökosystems.</p>
</li>
<li>
<p>Im Jahr 2021 erhielten wir weitere EU-Mittel für zwei “Next-Generation-Internet”-Anträge, nämlich für <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - E-Mail-Provider-Portabilitätsverzeichnis</a> (~97K EUR) und <a href="https://nlnet.nl/project/EmailPorting/">AEAP - E-Mail-Adressportierung</a> (~90K EUR). Ziel sind bessere Unterstützung von Mehrfachkonten, verbesserten QR-Code-Kontakt- und -Gruppen-Setups sowie Netzwerkverbesserungen auf allen Plattformen.</p>
</li>
<li>
<p>Von Ende 2021 bis März 2023 erhielten wir eine <em>Internet-Freedom</em>-Finanzierung (500K USD) vom U.S. Bureau of Democracy, Human Rights and Labor (DRL). Diese Finanzierung unterstützte unsere langjährigen Ziele, Delta Chat benutzerfreundlicher und kompatibel mit einer breiten Palette von E-Mail-Servern weltweit zu machen, sowie widerstandsfähiger und sicherer an Orten, die häufig von Internetzensur und Abschaltungen betroffen sind.</p>
</li>
<li>
<p>2023-2024 schlossen wir erfolgreich das vom OTF finanzierte
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure-Chatmail-Projekt</a> ab.
Dieses fügt “Garantierte Verschlüsselung”,
das <a href="https://delta.chat/chatmail">Chatmail-Server-Netzwerk</a>
und „Instant Onboarding“ allen ab April 2024 veröffentlichten Anwendungen hinzu.</p>
</li>
<li>
<p>2023 und 2024 wurden wir in das Next-Generation-Internet-Programm (NGI)
für unsere Arbeit an <a href="https://nlnet.nl/project/WebXDC-Push/">Webxdc-PUSH</a> aufgenommen,
@@ -1567,6 +1543,19 @@ zusammen mit Kooperationspartnern, die an
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
Alle diese Projekte sind teilweise abgeschlossen oder sollen Anfang 2025 abgeschlossen werden.</p>
</li>
<li>
<p>Im Jahr 2021 erhielten wir weitere EU-Mittel für zwei “Next-Generation-Internet”-Anträge, nämlich für <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - E-Mail-Provider-Portabilitätsverzeichnis</a> (~97K EUR) und <a href="https://nlnet.nl/project/EmailPorting/">AEAP - E-Mail-Adressportierung</a> (~90K EUR). Ziel sind bessere Unterstützung von Mehrfachkonten, verbesserten QR-Code-Kontakt- und -Gruppen-Setups sowie Netzwerkverbesserungen auf allen Plattformen.</p>
</li>
<li>
<p>Die <a href="https://nlnet.nl/">NLnet-Stiftung</a> bewilligte 2019/2020 46K EUR für die Fertigstellung von Rust-/Python-Bindungs und die Einrichtung eines Chat-Bot-Ökosystems.</p>
</li>
<li>
<p>Der <a href="https://opentechfund.org">Open Technology Fund</a> hat Delta Chat erstmals 2018/2019 bezuschusst; mit dieser Förderung (~$200K) wurden hauptsächlich die Android-App verbessert sowie das Release der Desktop-App in einer Betaversion ermöglicht. Basierend auf Nutzererfahrungen im Menschenrechtskontext wurden zudem verschiedene Funktionen entwickelt, siehe unseren Bericht <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
Die zweite Förderung 2019/2020 (~$300K) half uns bei der Erstellung der iOS-Version, unsere Kernbibliothek in die Programmiersprache “Rust” zu konvertieren und neue Funktionen für alle Plattformen bereitzustellen.</p>
</li>
<li>
<p>Das EU-Projekt <a href="https://nextleap.eu">NEXTLEAP</a> finanzierte 2017 und 2018 die Entwicklung und Implementierung von “Verifizierten Gruppen” und “Setup Kontakt” und half auch bei der Integration der Ende-zu-Ende-Verschlüsselung durch <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Manchmal erhalten wir einmalige Spenden von Privatpersonen, wofür wir sehr dankbar sind. Im Jahr 2021 hat uns zum Beispiel eine großzügige Privatperson 4000 EUR überwiesen mit dem Betreff “Weiter so!” 💜 Wir verwenden dieses Geld zur Finanzierung von Entwicklungstreffen oder zur Deckung von Ad-hoc-Ausgaben, die nicht ohne weiteres vorhersehbar sind oder nicht aus öffentlichen Fördermitteln erstattet werden können.
Der Erhalt von Spenden hilft uns auch, unabhängiger und langfristig lebensfähig zu werden, als Gemeinschaft.</p>
+38 -52
View File
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1663,52 +1663,38 @@ ordered chronologically:</p>
<ul>
<li>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Sometimes we receive one-time donations from private individuals.
+38 -52
View File
@@ -1184,6 +1184,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1365,12 +1369,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1446,7 +1448,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1457,12 +1459,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1472,12 +1471,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1652,52 +1652,38 @@ ordered chronologically:</p>
<ul>
<li>
<p>El proyecto de la UE <a href="https://nextleap.eu">NEXTLEAP</a> financió la investigación
e implementación de grupos verificados y protocolos de contacto
en 2017 y 2018 y también ayudó a integrar el cifrado de extremo a extremo
a través de <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>El proyecto de la UE <a href="https://nextleap.eu">NEXTLEAP</a> financió la investigación
e implementación de grupos verificados y protocolos de contacto
en 2017 y 2018 y también ayudó a integrar el cifrado de extremo a extremo
a través de <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Sometimes we receive one-time donations from private individuals.
+35 -48
View File
@@ -1175,6 +1175,9 @@ to exchange encryption setup information through QR-code scanning or “invite l
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, qui est prévu pour 2026, amènera un chiffrement avec résistance post-quantique ainsi que la confidentialité persistante (“forward secrecy”).</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
chat</a>
@@ -1358,12 +1361,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1418,7 +1419,7 @@ For example, tapping a link exposes IP Addresses to unknown parties and is the b
</h3>
<p>No, not yet.</p>
<p>Non, pas encore.</p>
<p>The Signal messenger introduced <a href="https://signal.org/blog/sealed-sender/">“Sealed Sender” in 2018</a>
to keep their server infrastructure ignorant of who is sending a message to a set of recipients.
@@ -1439,7 +1440,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Pas encore mais cela arrive avec <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1450,12 +1451,8 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, prévu pour 2026, permettra la suppression fiable (forward secrecy) grâce à une rotation automatique des clefs.
Cette approche est détaillée dans le brouillon du <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificat Autocrypt v2 OpenPGP</a>.</p>
<h3 id="pqc">
@@ -1465,12 +1462,11 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Pas encore mais cela arrive avec <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, prévu pour 2026, amènera un chiffrement avec résistance post-quantique pour protéger contre les attaques effectuées par des ordinateurs quantiques.
Delta Chat utilise la librairie Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a> qui supporte les dernières <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
Limplémentation est détaillée dans le brouillon du <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificat Autocrypt v2 OpenPGP</a>.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1628,47 +1624,38 @@ ordered chronologically:</p>
<ul>
<li>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Lassociation <a href="https://opentechfund.org">Open Technology Fund</a> nous a octroyé une première subvention en 2018/2019 (de 200.000$ environ), grâce à laquelle nous avons pu apporter des améliorations majeures à lapplication Android et publier une première version Beta de lapplication de bureau. Elle nous a aussi permis dancrer notre recherche UX de développement de fonctionnalités dans des contextes de droits humains. <br />
À ce sujet, vous pouvez consulter notre rapport en anglais <a href="https://delta.chat/en/2019-07-19-uxreport">“Needfinding and UX report”</a>.
La seconde subvention de 2019/2020 (environ 300.000$) nous a permis de publier des version iOS de Delta Chat, de convertir notre bibliothèque principale en Rust et de créer de nouvelles fonctionnalités pour toutes les plateformes.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Nous recevons parfois des dons ponctuels de la part de personnes privées.
+4
View File
@@ -6,6 +6,10 @@ a {
text-decoration: none;
}
a[href^='http']::after {
content: " ↗";
}
h2, h3, h4 {
margin-top: 2rem;
}
+38 -52
View File
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1663,52 +1663,38 @@ ordered chronologically:</p>
<ul>
<li>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Terkadang kami menerima sumbangan satu kali dari perorangan.
+32 -42
View File
@@ -1173,6 +1173,10 @@ per scambiare informazioni sulla configurazione della crittografia tramite la sc
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> viene utilizzato per stabilire
automaticamente la crittografia end-to-end tra i contatti e tutti i membri di una chat di gruppo.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
introdurrà una crittografia post-quantistica resistente e una segretezza avanzata.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Condivisione di un contatto con una
@@ -1436,7 +1440,7 @@ ma unimplementazione non è stata ancora concordata come priorità.</p>
</h3>
<p>No, non ancora.</p>
<p>Non ancora, ma arriverà con <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat al momento non supporta la tecnologia Perfect Forward Secrecy (PFS).
Ciò significa che se la tua chiave di decrittazione privata viene divulgata
@@ -1447,12 +1451,9 @@ In caso contrario, chi ottiene le tue chiavi di decrittazione
in genere è in grado di ottenere anche tutti i tuoi messaggi non eliminati
e non ha nemmeno bisogno di decifrare i messaggi raccolti in precedenza.</p>
<p>Abbiamo progettato un approccio Forward Secrecy che ha superato
lesame iniziale di alcuni crittografi ed esperti di implementazione
ma è in attesa di una stesura più formale
per accertarne laffidabilità nella messaggistica federata e nellutilizzo su più dispositivi,
prima di poter essere implementato in <a href="https://github.com/chatmail/core">chatmail core</a>,
che lo renderebbe disponibile in tutti i <a href="https://chatmail.at/clients">clients di chatmail</a>.</p>
<p><a href="https://autocrypt2.org">autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
garantirà uneliminazione affidabile (segretezza in avanti) tramite rotazione automatica delle chiavi.
Questo approccio è specificato nella bozza dei <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">certificati OpenPGP di Autocrypt v2</a>.</p>
<h3 id="pqc">
@@ -1462,12 +1463,13 @@ che lo renderebbe disponibile in tutti i <a href="https://chatmail.at/clients">c
</h3>
<p>No, non ancora.</p>
<p>Non ancora, ma arriverà con <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat utilizza la libreria Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, la cui piena implementazione è prevista per il 2026,
offrirà una crittografia post-quantistica resistente per proteggere dagli attacchi ai computer quantistici.
Delta Chat utilizza la libreria Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
che supporta lultima <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">bozza IETF Post-Quantum-Cryptography OpenPGP</a>.
Il nostro obiettivo è aggiungere il supporto PQC nel <a href="https://github.com/chatmail/core">core di chatmail</a> dopo che la bozza sarà stata finalizzata dallIETF
in collaborazione con altri implementatori di OpenPGP.</p>
Limplementazione è specificata nella bozza dei <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Certificati OpenPGP Autocrypt v2</a>.</p>
<h3 id="come-posso-controllare-manualmente-le-informazioni-di-crittografia">
@@ -1647,10 +1649,21 @@ ordinate cronologicamente:</p>
<ul>
<li>
<p>Il progetto UE <a href="https://nextleap.eu">NEXTLEAP</a> ha finanziato la ricerca
e implementazione di gruppi verificati e impostazione di protocolli di contatto
nel 2017 e nel 2018 e ha anche contribuito a integrare la crittografia end-to-end
tramite <a href="https://autocrypt.org">Autocrypt</a>.</p>
<p>Nel 2023 e nel 2024 siamo stati accettati nel programma Next Generation Internet (NGI)
per il nostro lavoro in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
insieme ai partner di collaborazione che lavorano su
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> e
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
Tutti questi progetti sono parzialmente completati o saranno completati allinizio del 2025.</p>
</li>
<li>
<p>Nel 2021 abbiamo ricevuto ulteriori finanziamenti dallUE per due proposte di Next-Generation-Internet, ovvero per <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - directory di portabilità dei provider di posta elettronica</a> (~97.000 EUR) e <a href="https://nlnet.nl/project/EmailPorting/">AEAP - portabilità degli indirizzi email</a> (~90.000 EUR), che hanno portato a un migliore supporto multi-profilo, a un miglioramento delle impostazioni di contatto e di gruppo tramite codice QR e a numerosi miglioramenti di rete su tutte le piattaforme.</p>
</li>
<li>
<p>La <a href="https://nlnet.nl/">fondazione NLnet</a> ha concesso nel 2019/2020 46.000 EUR per
completando i collegamenti Rust/Python e avviando un ecosistema Chat-bot.</p>
</li>
<li>
<p>L<a href="https://opentechfund.org">Open Technology Fund</a> ci ha dato una
@@ -1663,33 +1676,10 @@ rilasciare nelle versioni Delta/iOS, per convertire la nostra libreria principal
per fornire nuove funzionalità per tutte le piattaforme.</p>
</li>
<li>
<p>La <a href="https://nlnet.nl/">fondazione NLnet</a> ha concesso nel 2019/2020 46.000 EUR per
completando i collegamenti Rust/Python e avviando un ecosistema Chat-bot.</p>
</li>
<li>
<p>Nel 2021 abbiamo ricevuto ulteriori finanziamenti dallUE per due proposte di Next-Generation-Internet, ovvero per <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - directory di portabilità dei provider di posta elettronica</a> (~97.000 EUR) e <a href="https://nlnet.nl/project/EmailPorting/">AEAP - portabilità degli indirizzi email</a> (~90.000 EUR), che hanno portato a un migliore supporto multi-profilo, a un miglioramento delle impostazioni di contatto e di gruppo tramite codice QR e a numerosi miglioramenti di rete su tutte le piattaforme.</p>
</li>
<li>
<p>Da fine 2021 a marzo 2023 abbiamo ricevuto un finanziamento <em>Internet Freedom</em> (500.000 USD) dallUfficio per la Democrazia, i Diritti Umani e il Lavoro (DRL) degli Stati Uniti.
Questo finanziamento ha supportato i nostri obiettivi a lungo termine: rendere Delta Chat più utilizzabile
e compatibile con unampia gamma di server email in tutto il mondo, e più resiliente e sicura
in luoghi spesso colpiti da censura e blocchi di Internet.</p>
</li>
<li>
<p>2023-2024 abbiamo completato con successo il progetto <a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Chatmail Sicuro</a> finanziato da OTF,
consentendoci di introdurre la crittografia garantita,
creando una <a href="https://delta.chat/chatmail">rete di server di chatmail</a>
e fornendo “inserimento immediato” in tutte le app rilasciate da aprile 2024 in poi.</p>
</li>
<li>
<p>Nel 2023 e nel 2024 siamo stati accettati nel programma Next Generation Internet (NGI)
per il nostro lavoro in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
insieme ai partner di collaborazione che lavorano su
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> e
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
Tutti questi progetti sono parzialmente completati o saranno completati allinizio del 2025.</p>
<p>Il progetto UE <a href="https://nextleap.eu">NEXTLEAP</a> ha finanziato la ricerca
e implementazione di gruppi verificati e impostazione di protocolli di contatto
nel 2017 e nel 2018 e ha anche contribuito a integrare la crittografia end-to-end
tramite <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>A volte riceviamo donazioni una tantum da privati.
+38 -52
View File
@@ -1182,6 +1182,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1366,12 +1370,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1447,7 +1449,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1458,12 +1460,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1473,12 +1472,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1657,52 +1657,38 @@ ordered chronologically:</p>
<ul>
<li>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p><a href="https://opentechfund.org">Open Technology Fund</a> heeft twee subsidies toegekend.
De eerste subsidie, voor 2018/2019, ter waarde van ong. $200,000, heeft enorm geholpen om de Android-app
te verbeteren en een bètaversie van de computerclient vrij te geven.
Verder hebben we onderzoek kunnen doen naar het uiterlijk in relatie tot mensenrechten -
bekijk onze conclusie hier: <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
De tweede subsidie, voor 2019/2020, ter waarde van ong. $300,000, loopt nog en ondersteunt ons bij het
vrijgeven van de iOS-client, het overzetten van de code van de kernbibliotheek naar Rust en
het implementeren van nieuwe functies op alle platformen.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Soms ontvangen we eenmalige donaties van privépersonen, waar we
+25 -29
View File
@@ -105,8 +105,7 @@
</h2>
<p>Delta Chat is a reliable, decentralized and secure instant messaging app,
available for mobile and desktop platforms.</p>
<p>Delta Chat to niezawodna, zdecentralizowana i bezpieczna aplikacja do błyskawicznego przesyłania wiadomości, dostępna na platformy mobilne i stacjonarne.</p>
<ul>
<li>
@@ -1103,6 +1102,10 @@ weekly statistics will be automatically sent to a bot.</p>
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> służy do automatycznego ustanawiania szyfrowania typu end-to-end między kontaktami a wszystkimi członkami czatu grupowego.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Udostępnienie kontaktu na czacie</a> umożliwia odbiorcom korzystanie z szyfrowania typu end-to-end z tym kontaktem.</p>
</li>
@@ -1237,12 +1240,10 @@ even if the message was not end-to-end encrypted.</p>
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>Wszystkie pozostałe metadane dotyczące wiadomości, kontaktów i grup znajdują się w zaszyfrowanej metodą end-to-end części wiadomości.</p>
<h3 id="device-seizure">
@@ -1315,11 +1316,13 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Nie, jeszcze nie.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat obecnie nie obsługuje mechanizmu Perfect Forward Secrecy (PFS). Oznacza to, że jeśli twój prywatny klucz deszyfrujący zostanie ujawniony, a ktoś zdobędzie twoje wcześniejsze wiadomości w trakcie transmisji, będzie mógł je odszyfrować i odczytać za pomocą ujawnionego klucza deszyfrującego. Należy pamiętać, że mechanizm Forward Secrecy zwiększa bezpieczeństwo tylko w przypadku usuwania wiadomości. W przeciwnym razie osoba, która uzyska twoje klucze deszyfrujące, zazwyczaj będzie mogła uzyskać dostęp do wszystkich nieusuniętych wiadomości i nie będzie musiała odszyfrowywać żadnych wcześniej zebranych wiadomości.</p>
<p>Opracowaliśmy metodę Forward Secrecy, która przeszła wstępną analizę niektórych kryptografów i ekspertów ds. wdrożeń, ale oczekuje na bardziej formalne opracowanie, które potwierdzi jej niezawodne działanie w federacyjnym przesyłaniu wiadomości i w przypadku korzystania z wielu urządzeń, zanim zostanie zaimplementowana w <a href="https://github.com/chatmail/core">rdzeniu chatmail</a>, co uczyniłoby ją dostępną we wszystkich <a href="https://chatmail.at/clients">klientach chatmail</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1329,9 +1332,13 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Nie, jeszcze nie.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat korzysta z biblioteki Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>, która obsługuje najnowszy <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">projekt OpenPGP IETF Post-Quantum-Cryptography</a>. Planujemy dodać obsługę PQC do <a href="https://github.com/chatmail/core">rdzenia chatmail</a> po sfinalizowaniu projektu w IETF we współpracy z innymi implementatorami OpenPGP.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="jak-mogę-ręcznie-sprawdzić-informacje-o-szyfrowaniu">
@@ -1464,31 +1471,20 @@ Raczej korzystamy z publicznych źródeł finansowania, jak dotąd pochodzących
<ul>
<li>
<p>Unijny projekt <a href="https://nextleap.eu">NEXTLEAP</a> sfinansował badania i wdrożenie zweryfikowanych grup i ustawień protokołów kontaktowych w latach 2017 i 2018, a także pomógł zintegrować szyfrowanie end-to-end poprzez <a href="https://autocrypt.org">Autocrypt</a>.</p>
<p>W latach 2023 i 2024 zostaliśmy przyjęci do programu Next Generation Internet (NGI) za naszą pracę w <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, wraz z partnerami współpracującymi pracującymi nad <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> i <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Wszystkie te projekty są częściowo ukończone lub zostaną ukończone na początku 2025 r.</p>
</li>
<li>
<p>W 2021 r. otrzymaliśmy kolejne dofinansowanie z UE na dwie propozycje dotyczące Internetu nowej generacji, a mianowicie na <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD katalog przenośności dostawcy poczty e-mail</a> ( ~97 tys. EUR) i <a href="https://nlnet.nl/project/EmailPorting/">AEAP przenoszenie adresu e-mail</a> (~90 tys. EUR), co zaowocowało lepszą obsługą wielu kont, ulepszonymi kontaktami i ustawieniami grup za pomocą kodów QR oraz wieloma ulepszeniami sieciowymi na wszystkich platformach.</p>
</li>
<li>
<p><a href="https://nlnet.nl/">Fundacja NLnet</a> przekazała w latach 2019/2020 kwotę 46 tys. EUR na wykonanie wiązań Rust/Python i uruchomienie ekosystemu Chat-bot.</p>
</li>
<li>
<p><a href="https://opentechfund.org">Open Technology Fund</a> przyznał nam pierwszy grant w 2018/2019 (~200 000 $), dzięki któremu znacznie ulepszyliśmy aplikację na Androida i wydaliśmy pierwszą wersję beta aplikacji na komputery stacjonarne, a także ugruntował rozwój naszych funkcji w badaniach UX w kontekście praw człowieka, zobacz nasz końcowy raport <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX</a>.
Druga dotacja w 2019/2020 (~300 000 4) pomogła nam wydać wersje Delta/iOS, przekonwertować naszą podstawową bibliotekę na Rust i zapewnić nowe funkcje dla wszystkich platform.</p>
</li>
<li>
<p><a href="https://nlnet.nl/">Fundacja NLnet</a> przekazała w latach 2019/2020 kwotę 46 tys. EUR na wykonanie wiązań Rust/Python i uruchomienie ekosystemu Chat-bot.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
</li>
<li>
<p>W latach 2023-2024 pomyślnie ukończyliśmy finansowany przez OTF <a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">projekt Secure Chatmail</a>, co pozwoliło nam wprowadzić gwarantowane szyfrowanie, stworzyć <a href="https://delta.chat/chatmail">sieć serwerów chatmail</a> i zapewnić „natychmiastowe wdrażanie” we wszystkich aplikacjach wydanych od kwietnia 2024 r.</p>
</li>
<li>
<p>W latach 2023 i 2024 zostaliśmy przyjęci do programu Next Generation Internet (NGI) za naszą pracę w <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, wraz z partnerami współpracującymi pracującymi nad <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> i <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Wszystkie te projekty są częściowo ukończone lub zostaną ukończone na początku 2025 r.</p>
<p>Unijny projekt <a href="https://nextleap.eu">NEXTLEAP</a> sfinansował badania i wdrożenie zweryfikowanych grup i ustawień protokołów kontaktowych w latach 2017 i 2018, a także pomógł zintegrować szyfrowanie end-to-end poprzez <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Czasami otrzymujemy jednorazowe darowizny od osób prywatnych. Na przykład w 2021 roku pewna hojna osoba przekazała nam 4K EUR w formie przelewu bankowego tytułem “kontynuujcie dobry rozwój!”. 💜 Takie pieniądze przeznaczamy na finansowanie spotkań rozwojowych lub na doraźne wydatki, których nie da się łatwo przewidzieć lub zrefundować z publicznych dotacji. Otrzymywanie większej ilości darowizn pomaga nam również stać się bardziej niezależnymi i długoterminowo rentownymi jako społeczność współpracowników.</p>
+253 -268
View File
@@ -2,36 +2,36 @@
<html lang="pt"><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="#o-que-é-o-delta-chat">O que é o 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">Como posso encontrar pessoas para conversar?</a></li>
<li><a href="#por-que-um-chat-está-marcado-como-solicitação">Por que um chat está marcado como “Solicitação”?</a></li>
<li><a href="#como-posso-colocar-dois-dos-meus-amigos-em-contato-um-com-o-outro">Como posso colocar dois dos meus amigos em contato um com o outro?</a></li>
<li><a href="#dá-para-mandar-imagens-vídeos-e-outros-anexos-pelo-delta-chat">Dá para mandar imagens, vídeos e outros anexos pelo Delta Chat?</a></li>
<li><a href="#multiple-accounts">What are profiles? How can I switch between them?</a></li>
<li><a href="#multiple-accounts">O que são perfis? Como posso alternar entre eles?</a></li>
<li><a href="#quem-consegue-ver-a-imagem-do-meu-perfil">Quem consegue ver a imagem do meu perfil?</a></li>
<li><a href="#signature">Can I set a Bio/Status with Delta Chat?</a></li>
<li><a href="#what-do-pinning-muting-and-archiving-mean">What do Pinning, Muting and Archiving mean?</a></li>
<li><a href="#save">How do “Saved Messages” work?</a></li>
<li><a href="#what-does-the-green-dot-mean">What does the green dot mean?</a></li>
<li><a href="#signature">Posso definir uma Bio/Assinatura com o Delta Chat?</a></li>
<li><a href="#o-que-significa-fixar-silenciar-e-arquivar">O que significa Fixar, Silenciar e Arquivar?</a></li>
<li><a href="#save">Como funcionam as “Mensagens salvas”?</a></li>
<li><a href="#o-que-significa-o-ponto-verde">O que significa o ponto verde?</a></li>
<li><a href="#o-que-significam-os-carrapatos-mostrados-ao-lado-das-mensagens-de-saída">O que significam os carrapatos mostrados ao lado das mensagens de saída?</a></li>
<li><a href="#edit">Correct typos and delete messages after sending</a></li>
<li><a href="#ephemeralmsgs">How do disappearing messages work?</a></li>
<li><a href="#delold">What happens if I turn on “Delete Messages from Device”?</a></li>
<li><a href="#remove-account">How can I delete my chat profile?</a></li>
<li><a href="#edit">Corrigir erros de digitação e excluir mensagens após o envio</a></li>
<li><a href="#ephemeralmsgs">Como funcionam as mensagens efêmeras?</a></li>
<li><a href="#delold">O que acontece se eu ativar a opção “Apagar mensagens do dispositivo”?</a></li>
<li><a href="#remove-account">Como posso excluir minha conta?</a></li>
</ul>
</li>
<li><a href="#groups">Groups</a>
<li><a href="#groups">Grupos</a>
<ul>
<li><a href="#criação-de-um-grupo">Criação de um grupo</a></li>
<li><a href="#addmembers">Add and remove members</a></li>
<li><a href="#addmembers">Adicionar e remover membros</a></li>
<li><a href="#deletei-minha-própria-conta-por-acidente">Deletei minha própria conta por acidente.</a></li>
<li><a href="#não-quero-mais-receber-as-mensagens-de-um-grupo">Não quero mais receber as mensagens de um grupo.</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="#clonagem-de-grupos">Clonagem de grupos</a></li>
<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="#webxdc">In-chat apps</a>
<li><a href="#webxdc">Aplicativos embutidos</a>
<ul>
<li><a href="#where-can-i-get-in-chat-apps">Where can I get in-chat apps?</a></li>
<li><a href="#onde-posso-obter-aplicativos-embutidos">Onde posso obter aplicativos embutidos?</a></li>
<li><a href="#how-private-are-in-chat-apps">How private are in-chat apps?</a></li>
<li><a href="#create-xdc">How can I create my own in-chat apps?</a></li>
</ul>
@@ -105,118 +105,118 @@
</h2>
<p>Delta Chat is a reliable, decentralized and secure instant messaging app,
available for mobile and desktop platforms.</p>
<p>Delta Chat é um aplicativo de mensagens instantâneas confiável, descentralizado e seguro,
disponível para desktop e plataformas móveis.</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>Criação instantânea de **perfis de chat privados **
com <a href="https://chatmail.at/relays">relays chatmail</a> seguros e interoperáveis
que oferecem entrega de mensagens instantâneas e Notificações Push para dispositivos iOS e Android.</p>
</li>
<li>
<p>Pervasive <a href="#multiple-accounts">multi-profile</a> and
<a href="#multiclient">multi-device</a> support on all platforms
and between different <a href="https://chatmail.at/clients">chatmail apps</a>.</p>
<p>Suporte a <a href="#multiple-accounts">vários perfis</a> e
a <a href="#multiclient">vários dispositivos</a> em todas as plataformas
e entre diferentes <a href="https://chatmail.at/clients">aplicativos de chatmail</a>.</p>
</li>
<li>
<p>Interactive <a href="#webxdc">in-chat apps</a> for gaming and collaboration</p>
<p><a href="#webxdc">Aplicativos interativos embutidos</a> para jogos e colaboração</p>
</li>
<li>
<p><a href="#security-audits">Audited end-to-end encryption</a>
safe against network and server attacks.</p>
<p><a href="#security-audits">Criptografia de ponta a ponta auditada</a>
segura contra ataques à rede e ao servidor.</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>Software gratuito e de código aberto, tanto no aplicativo quanto no servidor,
desenvolvido com base nos <a href="https://github.com/chatmail/core/blob/main/standards.md#standards-used-in-delta-chat">Padrões da Internet</a>.</p>
</li>
</ul>
<h3 id="howtoe2ee">
How can I find people to chat with? <a href="#howtoe2ee" class="anchor"></a>
Como posso encontrar pessoas para conversar? <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>Primeiro, observe que o Delta Chat é um mensageiro privado.
Não há descoberta pública, <em>você</em> decide sobre seus contatos.</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>Se você estiver <strong>cara a cara</strong> com seu amigo ou parente,
toque no ícone de <strong>QR Code</strong> <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" />
na tela principal.<br />
Peça ao seu parceiro para <strong>escanear</strong> o QR code
com o aplicativo Delta Chat dele.</p>
</li>
<li>
<p>For a <strong>remote</strong> contact setup,
from the same screen,
click “Copy” orShareand send the <strong>invite link</strong>
through another private chat.</p>
<p>Para criar um contato <strong>remotamente</strong>,
na mesma tela,
clique em “Copiar” ouCompartilhar” e envie o <strong>link do convite</strong>
por meio de outro mensageiro privado.</p>
</li>
</ul>
<p>Now wait while connection gets established.</p>
<p>Agora, aguarde enquanto a conexão se estabelece.</p>
<ul>
<li>
<p>If both sides are online, they will soon see a chat
and can start messaging securely.</p>
<p>Se ambos os lados estiverem on-line, vocês logo verão um chat
e poderão começar a enviar mensagens com segurança.</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>Se um lado estiver off-line ou em uma rede ruim,
a capacidade de acessar o chat será atrasada até que a conectividade seja restaurada.</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>Parabéns!
Agora você usará automaticamente a <a href="#e2ee">criptografia de ponta a ponta</a> com esse contato.
Se vocês se adicionarem a <a href="#groups">grupos</a>, a criptografia de ponta a ponta será estabelecida entre todos os membros.</p>
<h3 id="why-is-a-chat-marked-as-request">
<h3 id="por-que-um-chat-está-marcado-como-solicitação">
Why is a chat marked as “Request”? <a href="#why-is-a-chat-marked-as-request" class="anchor"></a>
Por que um chat está marcado como “Solicitação”? <a href="#por-que-um-chat-está-marcado-como-solicitação" 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>Por ser um mensageiro privado,
somente amigos e familiares com quem você <a href="#howtoe2ee">compartilha seu QR code ou link de convite</a> podem escrever para você.</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>Seus amigos podem compartilhar seu contato com outros amigos,
isso aparece como <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Solicitação</b></p>
<ul>
<li>
<p>You need to <strong>accept</strong> the request before you can reply.</p>
<p>Você precisa <strong>aceitar</strong> a solicitação antes de poder responder.</p>
</li>
<li>
<p>You can also <strong>delete</strong> it if you dont want to chat with them for now.</p>
<p>Você também pode <strong>excluir</strong> a solicitação se não quiser conversar com a pessoa por enquanto.</p>
</li>
<li>
<p>If you delete a request, future messages from that contact will still appear
as message request, so you can change your mind. If you really dont want to
receive messages from this person, consider <strong>blocking</strong> them.</p>
<p>Se você excluir uma solicitação, as mensagens futuras desse contato ainda aparecerão
como solicitação de mensagem, assim você pode mudar de ideia. Se você realmente não quiser
receber mensagens dessa pessoa, considere <strong>bloqueá-la</strong>.</p>
</li>
</ul>
<h3 id="how-can-i-put-two-of-my-friends-in-contact-with-each-other">
<h3 id="como-posso-colocar-dois-dos-meus-amigos-em-contato-um-com-o-outro">
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>
Como posso colocar dois dos meus amigos em contato um com o outro? <a href="#como-posso-colocar-dois-dos-meus-amigos-em-contato-um-com-o-outro" 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>Anexe o primeiro contato ao chat do segundo clicando em <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Botão de Anexo → Contato</strong>.
Você também pode adicionar uma pequena mensagem de apresentação.</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>O segundo contato receberá um <strong>cartão</strong>
e poderá tocar nele para começar a conversar com o primeiro contato.</p>
<h3 id="dá-para-mandar-imagens-vídeos-e-outros-anexos-pelo-delta-chat">
@@ -226,31 +226,31 @@ and can tap it to start chatting with the first contact.</p>
</h3>
<p>Sim. Images, videos, files, voice messages etc. can be sent using the <img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Attachment-</strong>
or <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Voice Message</strong> buttons</p>
<p>Sim. Imagens, vídeos, arquivos, mensagens de voz etc. podem ser enviados usando os botões de<img style="vertical-align:middle; width:1.0em; margin:1px" src="../paperclip.png" alt="Paperclip" /> <strong>Anexo-</strong>
ou <img style="vertical-align:middle; width:0.8em; margin:1px" src="../mic.png" alt="Microphone" /> <strong>Mensagem de Voz</strong></p>
<p>For performance, images are optimized and sent at a smaller size by default, but you can send it as a “file” to preserve the original.</p>
<p>Para fins de desempenho, as imagens são otimizadas e enviadas em um tamanho menor por padrão, mas você pode enviá-las como um “arquivo” para preservar o original.</p>
<h3 id="multiple-accounts">
What are profiles? How can I switch between them? <a href="#multiple-accounts" class="anchor"></a>
O que são perfis? Como posso alternar entre eles? <a href="#multiple-accounts" class="anchor"></a>
</h3>
<p>A profile is <strong>a name, a picture</strong> and some additional information for encrypting messages.
A profile lives on your device(s) only
and uses the server only to relay messages.</p>
<p>Um perfil é <strong>um nome, uma foto</strong> e algumas informações adicionais para criptografar mensagens.
Um perfil reside apenas em seu(s) dispositivo(s)
e usa o servidor somente para transportar mensagens.</p>
<p>On first installation of Delta Chat a first profile is created.</p>
<p>Na primeira instalação do Delta Chat, é criado um primeiro perfil.</p>
<p>Later, you can tap your profile image in the upper left corner to <strong>Add Profiles</strong>
or to <strong>Switch Profiles</strong>.</p>
<p>Depois, você pode tocar em sua imagem de perfil no canto superior esquerdo para <strong>Adicionar Conta</strong>
ou para <strong>Mudar de Conta</strong>.</p>
<p>You may want to use separate profiles for political, family or work related activities.</p>
<p>Talvez você queira usar perfis separados para atividades políticas, familiares ou atividades do trabalho.</p>
<p>You may also wish to learn <a href="#multiclient">how to use the same profile on multiple devices</a>.</p>
<p>Talvez você também queira aprender <a href="#multiclient">como usar a mesma conta em vários dispositivos</a>.</p>
<h3 id="quem-consegue-ver-a-imagem-do-meu-perfil">
@@ -267,96 +267,96 @@ or to <strong>Switch Profiles</strong>.</p>
<h3 id="signature">
Can I set a Bio/Status with Delta Chat? <a href="#signature" class="anchor"></a>
Posso definir uma Bio/Assinatura com o Delta Chat? <a href="#signature" class="anchor"></a>
</h3>
<p>Yes,
you can do so under <strong>Settings → Profile → Bio</strong>.
Once you sent a message to a contact,
they will see it when they view your contact details.</p>
<p>Sim,
você pode fazer isso em <strong>Configurações → Conta → Assinatura</strong>.
Depois de enviar uma mensagem para um contato,
ele verá sua assinatura quando visualizar seus detalhes de contato.</p>
<h3 id="what-do-pinning-muting-and-archiving-mean">
<h3 id="o-que-significa-fixar-silenciar-e-arquivar">
What do Pinning, Muting and Archiving mean? <a href="#what-do-pinning-muting-and-archiving-mean" class="anchor"></a>
O que significa Fixar, Silenciar e Arquivar? <a href="#o-que-significa-fixar-silenciar-e-arquivar" class="anchor"></a>
</h3>
<p>Use these tools to organize your chats and keep everything in its place:</p>
<p>Use essas ferramentas para organizar seus chats e manter tudo em seu devido lugar:</p>
<ul>
<li>
<p><strong>Pinned chats</strong> always stay atop of the chat list. You can use them to access your most loved chats quickly or temporarily to not forget about things.</p>
<p>Os <strong>chats fixados</strong> sempre ficam no topo da lista de conversas. Você pode usá-los para acessar seus chats mais queridos rapidamente ou temporariamente para não se esquecer de nada.</p>
</li>
<li>
<p><strong>Mute chats</strong> if you do not want to get notifications for them. Muted chats stay in place and you can also pin a muted chat.</p>
<p><strong>Silencie os chats</strong> se não quiser receber notificações deles. Os bate-papos silenciados permanecem no lugar e você também pode fixar um chat silenciado.</p>
</li>
<li>
<p><strong>Archive chats</strong> if you do not want to see them in your chat list any longer.
They remain accessible above the chat list or via search
and are marked by <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Archived</b></p>
<p><strong>Arquive chats</strong> se não quiser mais vê-los em sua lista de conversas.
Eles permanecem acessíveis acima da lista conversa ou por meio de pesquisa
e são marcados como <b style="border: 1px solid currentColor; padding: 0 3px; font-size:90%">Arquivado</b></p>
</li>
<li>
<p>When an archived chat gets a new message, unless muted, it will <strong>pop out of the archive</strong> and back into your chat list.
<strong>Muted chats stay archived</strong> until you unarchive them manually.</p>
<p>Quando um chat arquivado receber uma nova mensagem, a menos que esteja silenciado, ele <strong>sairá do arquivo</strong> e voltará à sua lista de conversas.
<strong>Os chats silenciados permanecem arquivados</strong> até que você os desarquive manualmente.</p>
</li>
</ul>
<p>To use the functions, long tap or right click a chat in the chat list.</p>
<p>Para usar as funções, toque e segure ou clique com o botão direito do mouse em um conversa na lista conversas.</p>
<h3 id="save">
How do “Saved Messages” work? <a href="#save" class="anchor"></a>
Como funcionam as “Mensagens salvas”? <a href="#save" class="anchor"></a>
</h3>
<p><strong>Saved Messages</strong> is a chat that you can use to easily remember and find messages.</p>
<p><strong>Saved Messages</strong> é um chat que você pode usar para lembrar e localizar facilmente as mensagens.</p>
<ul>
<li>
<p>In any chat, long tap or right click a message and select <strong>Save</strong></p>
<p>Em qualquer chat, dê um toque longo ou clique com o botão direito do mouse em uma mensagem e selecione <strong>Salvar</strong></p>
</li>
<li>
<p>Saved messages are marked by the symbol
<img style="vertical-align:middle; width:1.2em; margin:1px" src="../saved-icon.png" alt="Saved icon" />
next to the timestamp</p>
<p>As mensagens salvas são marcadas com o símbolo
<img style="vertical-align:middle; width:1.2em; margin:1px" src="../saved-icon.png" alt="Saved icon" />
ao lado do registro de data e hora</p>
</li>
<li>
<p>Later, open the “Saved Messages” chat - and you will see the saved messages there.
By tapping <img style="vertical-align:middle; width:1.2em; margin:1px" src="../go-to-original.png" alt="Arrow-right icon" />,
you can go back to the original message in the original chat</p>
<p>Depois, abra o chat de “Mensagens salvas” - e você verá as mensagens salvas lá.
Ao tocar em <img style="vertical-align:middle; width:1.2em; margin:1px" src="../go-to-original.png" alt="Arrow-right icon" />,
você pode voltar para a mensagem original no chat original</p>
</li>
<li>
<p>Finally, you can also use “Saved Messages” to take <strong>personal notes</strong> - open the chat, type something, add a photo or a voice message etc.</p>
<p>Por fim, você também pode usar “Mensagens Salvas” para fazer <strong>notas pessoais</strong> - abra o chat, digite algo, adicione uma foto ou uma mensagem de voz etc.</p>
</li>
<li>
<p>As “Saved Message” are synced, they can become very handy for transferring data between devices</p>
<p>Como as “Mensagens salvas” são sincronizadas, elas podem se tornar muito úteis para a transferência de dados entre dispositivos</p>
</li>
</ul>
<p>Messages stay saved even if they are edited or deleted -
may it be by <a href="#edit">sender</a>, by <a href="#delold">device cleanup</a> or by <a href="#ephemeralmsgs">disappearing messages of other chats</a>.</p>
<p>As mensagens permanecem salvas mesmo que sejam editadas ou excluídas -
seja pelo <a href="#edit">remetente</a>, por <a href="#delold">limpeza do dispositivo</a> ou por <a href="#ephemeralmsgs">mensagens temporárias de outros chats</a>.</p>
<h3 id="what-does-the-green-dot-mean">
<h3 id="o-que-significa-o-ponto-verde">
What does the green dot mean? <a href="#what-does-the-green-dot-mean" class="anchor"></a>
O que significa o ponto verde? <a href="#o-que-significa-o-ponto-verde" class="anchor"></a>
</h3>
<p>You can sometimes see a <strong>green dot</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />
next to the avatar of a contact.
It means they were <strong>recently seen by you</strong> in the last 10 minutes,
e.g. because they messaged you or sent a read receipt.</p>
<p>Às vezes, você pode ver um <strong>ponto verde</strong> <img style="vertical-align:middle; width:1.2em; margin:1px" src="../green-dot.png" alt="" />
ao lado do avatar de um contato.
Isso significa que ele foi <strong>visto recentemente por você</strong> nos últimos 10 minutos,
por exemplo, porque ele enviou uma mensagem para você ou uma confirmação de leitura.</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>Portanto, essa não é uma checagem de status on-line em tempo real
e outras pessoas também nem sempre verão que você está “on-line”.</p>
<h3 id="o-que-significam-os-carrapatos-mostrados-ao-lado-das-mensagens-de-saída">
@@ -368,132 +368,131 @@ 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>Um tique</strong> <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick1.png" alt="" />
significa que a mensagem foi enviada com sucesso para o <a href="#relays">relay</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>Dois ticks** <img style="vertical-align:middle; width:1.5em; margin:1px" src="../tick2.png" alt="" />
indicam que seu contato leu a mensagem.</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>Em <a href="#groups">grupos</a>, o segundo tique significa que pelo menos um membro leu a mensagem.</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>Você só receberá o segundo tique se tanto você quanto um dos destinatários que leu a mensagem
tiverem <strong>Configurações → Conversas → Confirmação de leitura</strong> ativada.</p>
<h3 id="edit">
Correct typos and delete messages after sending <a href="#edit" class="anchor"></a>
Corrigir erros de digitação e excluir mensagens após o envio <a href="#edit" class="anchor"></a>
</h3>
<ul>
<li>
<p>You can edit the text of your messages after sending.
For that, long tap or right click the message and select <strong>Edit</strong>
or <img style="vertical-align:middle; width:1.2em; margin:1px" src="../edit-icon.png" alt="Edit icon" />.</p>
<p>Você pode editar o texto de suas mensagens após o envio.
Para isso, toque longamente ou clique com o botão direito do mouse na mensagem e selecione <strong>Editar Mensagem</strong>
ou <img style="vertical-align:middle; width:1.2em; margin:1px" src="../edit-icon.png" alt="Edit icon" />.</p>
</li>
<li>
<p>If you have sent a message accidentally,
from the same menu, select <strong>Delete</strong> and then <strong>Delete for Everyone</strong>.</p>
<p>Se você tiver enviado uma mensagem acidentalmente,
no mesmo menu, selecione <strong>Apagar</strong> e, em seguida, <strong>Apagar para Toda a Gente</strong>.</p>
</li>
</ul>
<p>While edited messages will have the word “Edited” next to the timestamp,
deleted messages will be removed without a marker in the chat.
Notifications are not sent and there is no time limit.</p>
<p>Enquanto as mensagens editadas terão a palavra “Editado” ao lado do registro de data e hora,
as mensagens excluídas serão removidas sem um marcador no chat.
Não são enviadas notificações e não há limite de tempo.</p>
<p>Note, that the original message may still be received by chat members
who could have already replied, forwarded, saved, screenshotted or otherwise copied the message.</p>
<p>Observe que a mensagem original ainda pode ser recebida pelos membros do conversa
que podem ter respondido, encaminhado, salvo, capturado a tela ou copiado a mensagem.</p>
<h3 id="ephemeralmsgs">
How do disappearing messages work? <a href="#ephemeralmsgs" class="anchor"></a>
Como funcionam as mensagens efêmeras? <a href="#ephemeralmsgs" class="anchor"></a>
</h3>
<p>You can turn on “disappearing messages”
in the settings of a chat,
at the top right of the chat window,
by selecting a time span
between 5 minutes and 1 year.</p>
<p>Você pode ativar a opção “mensagens temporárias”
nas configurações de um conversa,
no canto superior direito da janela conversa,
selecionando um intervalo de tempo
entre 5 minutos e 1 ano.</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>Até que a configuração seja desativada novamente,
o aplicativo Delta Chat de cada membro do conversa se encarrega de
de excluir as mensagens
após o período de tempo selecionado.
O intervalo de tempo começa
quando o destinatário vê a mensagem pela primeira vez no Delta Chat.
As mensagens são excluídas tanto
nos servidores
como nos próprios aplicativos.</p>
<p>Note that you can rely on disappearing messages
only as long as you trust your chat partners;
malicious chat partners can take photos,
or otherwise save, copy or forward messages before deletion.</p>
<p>Observe que você pode confiar no desaparecimento das mensagens
somente se você confiar em seus parceiros do conversa;
parceiros mal-intencionados de conversa podem tirar fotos,
ou salvar, copiar ou encaminhar mensagens antes de excluí-las.</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>Além disso,
se um parceiro de conversa desinstalar o Delta Chat,
as mensagens (de qualquer forma criptografadas) podem demorar mais para serem excluídas do servidor deles.</p>
<h3 id="delold">
What happens if I turn on “Delete Messages from Device”? <a href="#delold" class="anchor"></a>
O que acontece se eu ativar a opção “Apagar mensagens do dispositivo”? <a href="#delold" class="anchor"></a>
</h3>
<p>If you want to save storage on your device, you can choose to delete old
messages automatically.</p>
<p>Se quiser economizar espaço de armazenamento no dispositivo, você pode optar por excluir mensagens antigas automaticamente.</p>
<p>To turn it on, go to <strong>Settings → Chats → Delete Message from Device</strong>.
You can set a timeframe between “after an hour” and “after a year”;
this way, <em>all</em> messages will be deleted from your device as soon as they are
older than that.</p>
<p>Para ativá-la, vá para <strong>Configurações → Conversas → Apagar mensagens do dispositivo</strong>.
Você pode definir um período de tempo entre “após uma hora” e “após um ano”;
Dessa forma, <em>todas</em> as mensagens serão excluídas de seu dispositivo assim que forem
mais antigas do que isso.</p>
<h3 id="remove-account">
How can I delete my chat profile? <a href="#remove-account" class="anchor"></a>
Como posso excluir minha conta? <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>Se estiver usando mais de uma conta,
você pode removê-las individualmente no menu superior de mudar de conta (no Android e no iOS),
ou na barra lateral com um clique no botão direito do mouse (no aplicativo para desktop).
As contas são removidas somente no dispositivo em que a exclusão foi acionada.
Contas em outros dispositivos continuarão funcionando plenamente.</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>Se você usar uma conta única padrão, poderá simplesmente desinstalar o aplicativo.
Isso ainda acionará automaticamente a exclusão de todos os dados de endereço associados no servidor de chatmail.
Para obter mais informações, consulte <a href="https://nine.testrun.org/info.html#account-deletion">nine.testrun.org address-deletion</a>
ou a respectiva página do <a href="https://chatmail.at/relays">servidor de chatmail de terceiros</a> que você escolheu.</p>
<h2 id="groups">
Groups <a href="#groups" class="anchor"></a>
Grupos <a href="#groups" class="anchor"></a>
</h2>
<p>Groups let several people chat together privately with <strong>equal rights</strong>.</p>
<p>Os grupos permitem que várias pessoas conversem em particular com <strong>direitos iguais</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>Qualquer pessoa pode
alterar o nome do grupo ou o avatar,
<a href="#addmembers">adicionar ou remover membros</a>,
definir <a href="#ephemeralmsgs">mensagens temporárias</a>,
e <a href="#edit">excluir suas próprias mensagens</a> dos dispositivos de todos os membros.</p>
<p>Because all members have the same rights, groups work best among <strong>trusted friends and family</strong>.</p>
<p>Como todos os membros têm os mesmos direitos, os grupos funcionam melhor entre <strong>amigos e familiares de confiança</strong>.</p>
<h3 id="criação-de-um-grupo">
@@ -518,33 +517,33 @@ and <a href="#edit">delete their own messages</a> from all members devices.</
<h3 id="addmembers">
Add and remove members <a href="#addmembers" class="anchor"></a>
Adicionar e remover membros <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>Todos os membros do grupo têm os <strong>mesmos direitos</strong>.
Por este motivo, todos podem remover qualquer membro ou adicionar novos membros.</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>Para <strong>adicionar ou remover membros</strong>, toque no nome do grupo dentro do chat e selecione o membro a adicionar ou a remover.</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>Se o membro ainda não estiver na sua lista de contactos, mas estiver <strong>cara a cara</strong> com você,
na mesma tela, moste um <strong>QR code</strong>.<br />
Peça ao seu parceiro de conversa para <strong>escanear</strong> o QR code com o Delta Chat dele, tocando em
<img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> na tela inicial.</p>
</li>
<li>
<p>For a <strong>remote</strong> member addition,
click “Copy” orShareand send the <strong>invite link</strong>
through another private chat to the new member.</p>
<p>Para adicionar um membro <strong>remotamente</strong>,
clique em “Copiar” ouCompartilhar” e envie o link do <strong>convite</strong>
por meio de outro mensageiro privado para o novo membro.</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>O QR code e o link de convite podem ser usados para adicionar vários membros.
No entanto, como os grupos são <a href="#groups">destinados a pessoas de confiança</a>, evite compartilhá-los publicamente.</p>
<h3 id="deletei-minha-própria-conta-por-acidente">
@@ -571,60 +570,60 @@ Se você quiser entrar mais tarde no grupo novamente, peça a outra pessoa do gr
<p>Uma alternativa é “silenciar” um grupo. Fazendo isso, você receberá todas as mensagens e ainda poderá escrever, mas não será receberá mais notificações d enovas mensagens.</p>
<h3 id="cloning-a-group">
<h3 id="clonagem-de-grupos">
Cloning a group <a href="#cloning-a-group" class="anchor"></a>
Clonagem de grupos <a href="#clonagem-de-grupos" class="anchor"></a>
</h3>
<p>You can duplicate a group to start a separate discussion
or to exclude members without them noticing.</p>
<p>Você pode duplicar um grupo para iniciar uma discussão separada
ou para excluir membros sem que eles percebam.</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>Abra o perfil do grupo e toque em <strong>Clonar conversa</strong> (Android/iOS),
ou clique com o botão direito do mouse no grupo na lista de conversa (desktop).</p>
</li>
<li>
<p>Set a new name, choose an avatar, and adjust the member list if needed.</p>
<p>Defina um novo nome, escolha um avatar e ajuste a lista de membros, se necessário.</p>
</li>
</ul>
<p>The new group is <strong>fully independent</strong> from the original,
which continues to work as before.</p>
<p>O novo grupo é <strong>totalmente independente</strong> do original,
que continua funcionando como antes.</p>
<h3 id="how-many-members-can-participate-in-a-single-group">
<h3 id="quantos-membros-podem-participar-em-um-único-grupo">
How many members can participate in a single group? <a href="#how-many-members-can-participate-in-a-single-group" class="anchor"></a>
Quantos membros podem participar em um único grupo? <a href="#quantos-membros-podem-participar-em-um-único-grupo" class="anchor"></a>
</h3>
<p>There is no strict technical limit,
but more than 150 is not recommended.</p>
<p>Não há um limite técnico rigoroso,
mas não é recomendável mais de 150.</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>À medida que os grupos se tornam maiores, eles podem se tornar socialmente instáveis e podem precisar de uma hierarquia -
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="webxdc">
In-chat apps <a href="#webxdc" class="anchor"></a>
Aplicativos embutidos <a href="#webxdc" class="anchor"></a>
</h2>
<p>You can send apps to a chat - games, editors, polls and other tools.
This makes Delta Chat a truly extensible messenger.</p>
<p>Você pode enviar aplicativos para um chat - jogos, editores, enquetes e outras ferramentas.
Isso torna o Delta Chat um mensageiro realmente extensível.</p>
<h3 id="where-can-i-get-in-chat-apps">
<h3 id="onde-posso-obter-aplicativos-embutidos">
Where can I get in-chat apps? <a href="#where-can-i-get-in-chat-apps" class="anchor"></a>
Onde posso obter aplicativos embutidos? <a href="#onde-posso-obter-aplicativos-embutidos" class="anchor"></a>
</h3>
@@ -1184,6 +1183,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1368,12 +1371,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1449,7 +1450,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1460,12 +1461,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1475,12 +1473,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1659,52 +1658,38 @@ ordered chronologically:</p>
<ul>
<li>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Sometimes we receive one-time donations from private individuals.
+35 -47
View File
@@ -1181,6 +1181,10 @@ Chatmail использует INBOX по умолчанию для ретран
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> используется для автоматической
настройки сквозного шифрования между контактами и всеми членами группового чата.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
обеспечит поддержку постквантового шифрования и прямой секретности.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Обмен контактом в
@@ -1427,7 +1431,7 @@ Delta Chat вместо этого использует реализацию Ope
</h3>
<p>Нет, еще нет.</p>
<p>Нет, пока нет.</p>
<p>Мессенджер Signal внедрил функцию <a href="https://signal.org/blog/sealed-sender/">“Sealed Sender” (Засекреченный отправитель) в 2018 году</a>,
чтобы их серверная инфраструктура не имела информации о том, кто отправляет сообщение группе получателей.
@@ -1448,7 +1452,7 @@ Delta Chat вместо этого использует реализацию Ope
</h3>
<p>Нет, еще нет.</p>
<p>Пока нет, но это будет реализовано в <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>На данный момент, Delta Chat не поддерживает Perfect Forward Secrecy (PFS) (Совершенную прямую секретность).
Это означает, что если ваш приватный ключ дешифрования будет скомпрометирован,
@@ -1459,12 +1463,9 @@ Delta Chat вместо этого использует реализацию Ope
также может получить все ваши не удалённые сообщения
и ему даже не нужно расшифровывать какие-либо ранее собранные сообщения.</p>
<p>Мы разработали подход к Forward Secrecy (Прямой секретности), который прошёл
первичную проверку некоторыми криптографами и экспертами по реализации
но требует более формального описания
чтобы убедиться, что он надёжно работает в федеративном обмене сообщениями и при использовании нескольких устройств,
прежде чем он может быть внедрён в <a href="https://github.com/chatmail/core">ядро chatmail</a>,
что сделает его доступным во всех <a href="https://chatmail.at/clients">клиентах clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
обеспечит надёжное удаление (прямую секретность) за счёт автоматической ротации ключей.
Этот подход описан в черновике спецификации <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a>.</p>
<h3 id="pqc">
@@ -1474,12 +1475,13 @@ Delta Chat вместо этого использует реализацию Ope
</h3>
<p>Нет, еще нет.</p>
<p>Пока нет, но эта возможность появится в <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat использует библиотеку OpenPGP на Rust <a href="https://github.com/rpgp/rpgp">rPGP</a>,
которая поддерживает последний <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">черновик IETF Post-Quantum-Cryptography OpenPGP</a>.
Мы планируем добавить поддержку PQC в <a href="https://github.com/chatmail/core">ядро chatmail</a> после того, как черновик будет окончательно утвержден в IETF
в сотрудничестве с другими разработчиками OpenPGP.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, полное внедрение которого запланировано на 2026 год,
обеспечит поддержку постквантового шифрования для защиты от атак с использованием квантовых компьютеров.
Delta Chat использует Rust-библиотеку OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a>
которая поддерживает актуальный черновик IETF <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP</a>.
Особенности реализации описаны в черновике спецификации <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a>.</p>
<h3 id="как-можно-вручную-проверить-информацию-о-шифровании">
@@ -1658,10 +1660,22 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS и macOS App Store, Microsof
<ul>
<li>
<p>Проект ЕС <a href="https://nextleap.eu">NEXTLEAP</a> финансировал исследование
и внедрение проверенных групп и настройку протоколов контактов
в 2017 и 2018 годах, а также помог интегрировать сквозное шифрование
через <a href="https://autocrypt.org">Autocrypt</a>.</p>
<p>В 2023 и 2024 годах мы были приняты в программу Next Generation Internet (NGI)
за нашу работу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
в сотрудничестве с партнерами, работающими над
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> и
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
Все эти проекты частично завершены или будут завершены в начале 2025 года.</p>
</li>
<li>
<p>В 2021 г. мы получили дополнительное финансирование из ЕС для двух Next-Generation-Internet
целей, а именно для <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97 тыс. евро) и <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90 тыс. евро). Это привело к улучшению поддержки нескольких профилей, улучшению настройки контактов и групп с помощью QR-кода и многим улучшениям в сетевом взаимодействии на всех платформах.</p>
</li>
<li>
<p>Фонд <a href="https://nlnet.nl/">NLnet Foundation</a> выделил в 2019/2020 году 46 тысяч евро на
доработку связки Rust/Python и создание экосистемы чат-ботов..</p>
</li>
<li>
<p>Фонд <a href="https://opentechfund.org">Open Technology Fund</a> предоставил нам
@@ -1674,36 +1688,10 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS и macOS App Store, Microsof
предоставить новые функции для всех платформ.</p>
</li>
<li>
<p><a href="https://nlnet.nl/">Фонд NLnet</a> выделил в 2019/2020 году 46 тыс. евро на
завершение привязки Rust/Python и создание экосистемы чат-ботов.</p>
</li>
<li>
<p>В 2021 г. мы получили дополнительное финансирование из ЕС для двух Next-Generation-Internet
целей, а именно для <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97 тыс. евро) и <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90 тыс. евро). Это привело к улучшению поддержки нескольких профилей, улучшению настройки контактов и групп с помощью QR-кода и многим улучшениям в сетевом взаимодействии на всех платформах.</p>
</li>
<li>
<p>С конца 2021 года по март 2023 года мы получили финансирование в размере ($500 тыс.) от
U.S. Bureau of Democracy, Human Rights and Labor (DRL) для поддержки <em>свободы интернета</em>.
Это финансирование поддержало наши долгосрочные цели, сделать Delta Chat более удобным для использования
и совместимым с широким спектром электронных почтовых серверов по всему миру, а также более устойчивым
и безопасным в местах, часто подвергающихся интернет-цензуре и отключениям.</p>
</li>
<li>
<p>2023-2024 мы завершили проект финансируемый OTF
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
что позволило нам внедрить гарантированное шифрование,
создать сеть серверов <a href="https://delta.chat/chatmail">chatmail</a>
и обеспечить “немедленную регистрацию” во всех приложениях, выпущенных с апреля 2024 года.</p>
</li>
<li>
<p>В 2023 и 2024 годах мы были приняты в программу Next Generation Internet (NGI)
за нашу работу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
в сотрудничестве с партнерами, работающими над
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> и
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
Все эти проекты частично завершены или будут завершены в начале 2025 года.</p>
<p>Проект ЕС <a href="https://nextleap.eu">NEXTLEAP</a> финансировал исследование
и внедрение проверенных групп и настройку протоколов контактов
в 2017 и 2018 годах, а также помог интегрировать сквозное шифрование
через <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Иногда мы получаем разовые пожертвования от физических лиц.
+38 -52
View File
@@ -1188,6 +1188,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1372,12 +1376,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1453,7 +1455,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1464,12 +1466,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1479,12 +1478,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1663,52 +1663,38 @@ ordered chronologically:</p>
<ul>
<li>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
<p>The <a href="https://opentechfund.org">Open Technology Fund</a> gave us a
first 2018/2019 grant (~$200K) during which we majorly improved the Android app
and released a first Desktop app beta version, and which moreover
moored our feature developments in UX research in human rights contexts,
see our concluding <a href="https://delta.chat/en/2019-07-19-uxreport">Needfinding and UX report</a>.
The second 2019/2020 grant (~$300K) helped us to
release Delta/iOS versions, to convert our core library to Rust, and
to provide new features for all platforms.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Sometimes we receive one-time donations from private individuals.
+39 -50
View File
@@ -1190,6 +1190,10 @@ to exchange encryption setup information through QR-code scanning or “invite l
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
establishing end-to-end encryption between contacts and all members of a group chat.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Sharing a contact to a
@@ -1374,12 +1378,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1455,7 +1457,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1466,12 +1468,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1481,12 +1480,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1665,10 +1665,25 @@ ordered chronologically:</p>
<ul>
<li>
<p>The <a href="https://nextleap.eu">NEXTLEAP</a> EU project funded the research
and implementation of verified groups and setup contact protocols
in 2017 and 2018 and also helped to integrate end-to-end Encryption
through <a href="https://autocrypt.org">Autocrypt</a>.</p>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
</li>
<li>
<p>Më 2021-n morëm financime të mëtejshme nga BE për dy propozime që shtrihen në
“Internetin e Brezit Tjetër”, konkretisht për <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - e-mail provider portability directory</a> (~97K euro) dhe <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K euro) që sollën mbulim më të mirë për përdorues me shumë
llogari, përmirësim të gjërave për kontakte me kod QR dhe grupe, si dhe mjaft
përmirësime në punën në rrjet për krejt platformat.</p>
</li>
<li>
<p><a href="https://nlnet.nl/">Fondacioni NLnet</a> dhuroi 46K euro gjatë 2019/2020 për
plotësimin e <em>Rust/Python bindings</em> dhe për ti dhënë udhë një ekosistemi
Chat-bot.</p>
</li>
<li>
<p><a href="https://opentechfund.org">Open Technology Fund</a> na dha grantin e parë
@@ -1681,36 +1696,10 @@ versione Delta/iOS, për të shndërruar bibliotekën tonë bazë në Rust, si d
për të sjellë veçori të reja për krejt platformat.</p>
</li>
<li>
<p>The <a href="https://nlnet.nl/">NLnet foundation</a> granted in 2019/2020 EUR 46K for
completing Rust/Python bindings and instigating a Chat-bot eco-system.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
</li>
<li>
<p>2023-2024 we successfully completed the OTF-funded
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail project</a>,
allowing us to introduce guaranteed encryption,
creating a <a href="https://delta.chat/chatmail">chatmail server network</a>
and providing “instant onboarding” in all apps released from April 2024 on.</p>
</li>
<li>
<p>In 2023 and 2024 we got accepted in the Next Generation Internet (NGI)
program for our work in <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>,
along with collaboration partners working on
<a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>,
<a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>,
<a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> and
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>.
All of these projects are partially completed or to be completed in early 2025.</p>
<p>Projekti <a href="https://nextleap.eu">NEXTLEAP</a> i BE-së financoi kërkimin
për dhe sendërtimin e grupeve të verifikuara dhe protokolleve të
ujdisjes së kontakteve më 2017-n dhe 2018-n dhe ndihmoi gjithashtu
të integrohet Fshehtëzim Skaj-më-Skaj përmes <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Ndonjëherë marrim dhurime unike nga individë privatë.
+25 -33
View File
@@ -140,17 +140,10 @@
<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>Якщо ви перебуваєте <strong>віч-на-віч</strong> зі своїм другом або родиною, торкніться піктограми <strong>QR-код</strong> на головному екрані <img style="vertical-align:middle; height:1.3em; margin:1px" src="../qr-icon.png" /> на головному екрані. Попросіть вашого партнера по чату <strong>сканувати</strong> QR-зображення за допомогою їхнього застосунку 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>Для <strong>віддаленого</strong> налаштування контакту, на тому ж самому екрані, натисніть “Копіювати” або “Поділитися” і відправте <strong>запрошувальне посилання</strong> через інший приватний чат.</p>
</li>
</ul>
@@ -1095,6 +1088,10 @@ to send anonymous usage statistics.</p>
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> використовується для автоматичного встановлення наскрізного шифрування між контактами і всіма учасниками групового чату.</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">Поширення контакту в чаті</a> дозволяє отримувачам використовувати наскрізне шифрування з контактом.</p>
</li>
@@ -1298,11 +1295,13 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Ні, поки ще ні.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat наразі не підтримує ідеальну пряму секретність (Perfect Forward Secrecy, PFS). Це означає, що якщо ваш приватний ключ для розшифрування буде скомпрометовано, а хтось заздалегідь зібрав ваші повідомлення під час передачі, він зможе розшифрувати та прочитати їх, використовуючи зламаний ключ. Зверніть увагу, що пряма секретність підвищує рівень безпеки лише в тому разі, якщо ви видаляєте повідомлення. Інакше, якщо хтось отримує доступ до ваших ключів розшифрування, він зазвичай також має доступ до всіх ваших невидалених повідомлень і навіть не потребує розшифровувати заздалегідь перехоплені дані.</p>
<p>Ми розробили підхід Forward Secrecy, який витримав початкову експертизу від деяких криптографів та експертів з реалізації але чекає на більш офіційний звіт щоб переконатися, що він надійно працює в об’єднаних системах обміну повідомленнями та при використанні декількох пристроїв, перш ніж його можна буде реалізувати в <a href="https://github.com/chatmail/core">ядрі чату</a>, що зробить його доступним у всіх <a href="https://chatmail.at/clients">клієнтах чату</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1312,9 +1311,13 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>Ні, поки ще ні.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat використовує бібліотеку Rust OpenPGP <a href="https://github.com/rpgp/rpgp">rPGP</a> яка підтримує останню версію <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>. Ми плануємо додати підтримку PQC у <a href="https://github.com/chatmail/core">chatmail core</a> після того, як проект буде завершено у IETF у співпраці з іншими розробниками OpenPGP.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="як-я-можу-вручну-перевірити-інформацію-про-шифрування">
@@ -1445,7 +1448,14 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
<ul>
<li>
<p>Проект ЄС <a href="https://nextleap.eu">NEXTLEAP</a> фінансував дослідження та впровадження верифікованих груп і протоколів встановлення контактів у 2017 та 2018 роках, а також допоміг інтегрувати наскрізне шифрування через <a href="https://autocrypt.org">Autocrypt</a>.</p>
<p>У 2023 та 2024 роках нас прийняли до програми Next Generation Internet (NGI) за нашу роботу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, а також у співпраці з партнерами, які працюють над <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> та <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Усі ці проєкти частково завершені або будуть завершені на початку 2025 року.</p>
</li>
<li>
<p>У 2021 році ми отримали подальше фінансування від ЄС на дві пропозиції щодо Інтернету наступного покоління а саме на <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - каталог перенесення провайдерів електронної пошти</a> (~97 тис. євро) та <a href="https://nlnet.nl/project/EmailPorting/">AEAP - перенесення адрес електронної пошти</a> (~90 тис. євро), що дозволило нам покращити багатопрофільну підтримку, вдосконалити налаштування контактів та груп за допомогою QR-коду та багато інших мережевих покращень на всіх платформах.</p>
</li>
<li>
<p>Фонд <a href="https://nlnet.nl/">NLnet</a> виділив у 2019/2020 роках 46 тисяч євро на
завершення прив’язок Rust/Python та запуск екосистеми чат-ботів.</p>
</li>
<li>
<p><a href="https://opentechfund.org">Open Technology Fund</a> надав нам два гранти.
@@ -1458,25 +1468,7 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
і додати нові функції для всіх платформ.</p>
</li>
<li>
<p>Фонд <a href="https://nlnet.nl/">NLnet</a> виділив у 2019/2020 роках 46 тисяч євро на
завершення прив’язок Rust/Python та запуск екосистеми чат-ботів.</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
</li>
<li>
<p>У 2023-2024 роках ми успішно завершили проєкт <a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">Secure Chatmail</a>, що фінансувався OTF, що дозволило нам запровадити гарантоване шифрування, створити <a href="https://delta.chat/chatmail">мережу серверів chatmail</a> та забезпечити “миттєву реєстрацію” у всіх застосунках, випущених з квітня 2024 року.</p>
</li>
<li>
<p>У 2023 та 2024 роках нас прийняли до програми Next Generation Internet (NGI) за нашу роботу над <a href="https://nlnet.nl/project/WebXDC-Push/">webxdc PUSH</a>, а також у співпраці з партнерами, які працюють над <a href="https://nlnet.nl/project/Webxdc-Evolve/">webxdc evolve</a>, <a href="https://nlnet.nl/project/WebXDC-XMPP/">webxdc XMPP</a>, <a href="https://nlnet.nl/project/DeltaTouch/">DeltaTouch</a> та <a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a>. Усі ці проєкти частково завершені або будуть завершені на початку 2025 року.</p>
<p>Проект ЄС <a href="https://nextleap.eu">NEXTLEAP</a> фінансував дослідження та впровадження верифікованих груп і протоколів встановлення контактів у 2017 та 2018 роках, а також допоміг інтегрувати наскрізне шифрування через <a href="https://autocrypt.org">Autocrypt</a>.</p>
</li>
<li>
<p>Іноді ми отримуємо одноразові пожертви від приватних осіб. Наприклад, у 2021 році щедра приватна особа перерахував нам 4 тис. євро з повідомленням «так тримати!». 💜 Ми використовуємо такі пожертви для фінансування зборів на розвиток або для тимчасових витрат, які важко передбачити або відшкодувати за рахунок грантів державного фінансування. Отримання більшої кількості пожертв також допомагає нам стати більш незалежними та довгостроково життєздатними як спільнота контриб’юторів.</p>
+28 -43
View File
@@ -1161,6 +1161,10 @@ weekly statistics will be automatically sent to a bot.</p>
<li>
<p><a href="https://autocrypt.org">Autocrypt</a> is used for automatically
用于在联系人和群聊的所有成员之间自动建立端到端加密。</p>
</li>
<li>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption and forward secrecy.</p>
</li>
<li>
<p><a href="https://github.com/chatmail/core/blob/main/spec.md#attaching-a-contact-to-a-message">将联系人分享到聊天中
@@ -1342,12 +1346,10 @@ Instead, all group metadata is end-to-end encrypted and stored on end-user devic
<p>Servers can therefore only see:</p>
<ul>
<li>the sender and receiver addresses</li>
<li>and the message size.</li>
<li>Sender and receiver addresses, randomly generated by default</li>
<li>Message size</li>
</ul>
<p>By default, the addresses are randomly generated.</p>
<p>All other message, contact and group metadata resides in the end-to-end encrypted part of messages.</p>
<h3 id="device-seizure">
@@ -1423,7 +1425,7 @@ but an implementation has not been agreed as a priority yet.</p>
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat today doesnt support Perfect Forward Secrecy (PFS).
This means that if your private decryption key is leaked,
@@ -1434,12 +1436,9 @@ Otherwise, someone obtaining your decryption keys
is typically also able to get all your non-deleted messages
and doesnt even need to decrypt any previously collected messages.</p>
<p>We designed a Forward Secrecy approach that withstood
initial examination from some cryptographers and implementation experts
but is pending a more formal write up
to ascertain it reliably works in federated messaging and with multi-device usage,
before it could be implemented in <a href="https://github.com/chatmail/core">chatmail core</a>,
which would make it available in all <a href="https://chatmail.at/clients">chatmail clients</a>.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will provide reliable deletion (forward secrecy) through automatic key rotation.
This approach is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="pqc">
@@ -1449,12 +1448,13 @@ which would make it available in all <a href="https://chatmail.at/clients">chatm
</h3>
<p>No, not yet.</p>
<p>Not yet, but its coming with <a href="https://autocrypt2.org">Autocrypt v2</a>.</p>
<p>Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
We aim to add PQC support in <a href="https://github.com/chatmail/core">chatmail core</a> after the draft is finalized at the IETF
in collaboration with other OpenPGP implementers.</p>
<p><a href="https://autocrypt2.org">Autocrypt v2</a>, scheduled for full implementation in 2026,
will bring post-quantum resistant encryption to protect against quantum computer attacks.
Delta Chat uses the Rust OpenPGP library <a href="https://github.com/rpgp/rpgp">rPGP</a>
which supports the latest <a href="https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/">IETF Post-Quantum-Cryptography OpenPGP draft</a>.
The implementation is specified in the <a href="https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/">Autocrypt v2 OpenPGP Certificates</a> draft.</p>
<h3 id="how-can-i-manually-check-encryption-information">
@@ -1623,33 +1623,6 @@ Google Play Store, F-Droid, Huawei App Gallery, iOS and macOS App Store, Microso
按时间顺序排列:</p>
<ul>
<li>
<p><a href="https://nextleap.eu">NEXTLEAP</a>欧盟项目资助了以下研究和实施工作:在 2017 年和 2018 年实施的验证组和设置联系协议和通过 <a href="https://autocrypt.org">Autocrypt</a>整合了端到端加密。</p>
</li>
<li>
<p><a href="https://opentechfund.org">开放技术基金</a> 2018/2019 年提供的第一笔赠款(约 20 万美元)期间,我们显著改善了安卓应用,发布了第一个桌面测试版,并根据人权方面的用户体验研究进行了功能开发,请参阅我们的结论<a href="https://delta.chat/en/2019-07-19-uxreport">《需求发现与用户体验报告》</a>。2019/2020 年的第二笔赠款(约 30 万美元)对发布 Delta/iOS 版本,将核心库转换到 Rust ,以及为所有平台开发新功能提供了帮助。</p>
</li>
<li>
<p><a href="https://nlnet.nl/">NLnet 基金会</a> 2019/2020 年拨款 4.6 万欧元,用于完成 Rust/Python 绑定并建立聊天机器人生态系统。</p>
</li>
<li>
<p>In 2021 we received further EU funding for two Next-Generation-Internet
proposals, namely for <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - email provider portability directory</a> (~97K EUR) and <a href="https://nlnet.nl/project/EmailPorting/">AEAP - email address porting</a> (~90K EUR) which resulted in better multi-profile support, improved QR-code contact and group setups and many networking improvements on all platforms.</p>
</li>
<li>
<p>From End 2021 till March 2023 we received <em>Internet Freedom</em> funding (500K USD) from the
U.S. Bureau of Democracy, Human Rights and Labor (DRL).
This funding supported our long-running goals to make Delta Chat more usable
and compatible with a wide range of email servers world-wide, and more resilient and secure
in places often affected by internet censorship and shutdowns.</p>
</li>
<li>
<p>2023-2024 年,我们成功完成了 OTF 资助的
<a href="https://www.opentech.fund/projects-we-support/supported-projects/secure-chat-mail-with-delta-chat/">安全 Chatmail 项目</a>
使我们能够引入保证的加密,
创建一个 <a href="https://delta.chat/chatmail">Chatmail 服务器网络</a>
并在 2024 年 4 月起发布的所有应用中提供“即时入职”。</p>
</li>
<li>
<p>在 2023 年和 2024 年,我们的 <a href="https://nlnet.nl/project/WebXDC-Push/">WebXDC PUSH</a> 工作已在下一代互联网 (NGI) 中获得认可,
并与致力于
@@ -1659,6 +1632,18 @@ in places often affected by internet censorship and shutdowns.</p>
<a href="https://nlnet.nl/project/DeltaTauri/">DeltaTauri</a> 的合作伙伴合作。
所有这些项目都已部分完成或将在 2025 年初完成。</p>
</li>
<li>
<p>在 2021 年,我们从两项下一代互联网提案收到了欧盟的进一步资助,即 <a href="https://dapsi.ngi.eu/hall-of-fame/eppd/">EPPD - 电子邮件提供商可移植性目录</a>(约 9.7 万欧元)和 <a href="https://nlnet.nl/project/EmailPorting/">AEAP - 电子邮件地址移植</a>(约 9 万欧元)。这带来了更好的多账户支持,改进的二维码联系人和群组设置,和所有平台上的多处网络改进。</p>
</li>
<li>
<p><a href="https://nlnet.nl/">NLnet 基金会</a> 2019/2020 年拨款 4.6 万欧元,用于完成 Rust/Python 绑定并建立聊天机器人生态系统。</p>
</li>
<li>
<p><a href="https://opentechfund.org">开放技术基金</a> 2018/2019 年提供的第一笔赠款(约 20 万美元)期间,我们显著改善了安卓应用,发布了第一个桌面测试版,并根据人权方面的用户体验研究进行了功能开发,请参阅我们的结论<a href="https://delta.chat/en/2019-07-19-uxreport">《需求发现与用户体验报告》</a>。2019/2020 年的第二笔赠款(约 30 万美元)对发布 Delta/iOS 版本,将核心库转换到 Rust ,以及为所有平台开发新功能提供了帮助。</p>
</li>
<li>
<p><a href="https://nextleap.eu">NEXTLEAP</a>欧盟项目资助了以下研究和实施工作:在 2017 年和 2018 年实施的验证组和设置联系协议和通过 <a href="https://autocrypt.org">Autocrypt</a>整合了端到端加密。</p>
</li>
<li>
<p>我们有时会收到个人的一次性捐款。 例如,2021 年,一位慷慨的人士以 “继续保持良好的发展态势!”为主题通过银行向我们捐赠了4千欧元💜。 我们用这些钱来资助发展活动或支付不容易预测或从公共基金中报销的临时费用。收到更多的捐款也有助于我们作为一个贡献者社区变得更加独立和长期可持续。</p>
+67 -11
View File
@@ -211,11 +211,11 @@ public class Rpc {
}
/**
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
* Before this function is called, `checkQr()` should confirm the type of the
* QR code is `account` or `webrtcInstance`.
* Set configuration values from a QR code (technically from the URI stored in it).
* Before this function is called, `check_qr()` should be used to get the QR code type.
* <p>
* Internally, the function will call dc_set_config() with the appropriate keys,
* "DCACCOUNT:" and "DCLOGIN:" QR codes configure the account, but I/O mustn't be started for
* such QR codes, consider using [`Self::add_transport_from_qr`] which also restarts I/O.
*/
public void setConfigFromQr(Integer accountId, String qrContent) throws RpcException {
transport.call("set_config_from_qr", mapper.valueToTree(accountId), mapper.valueToTree(qrContent));
@@ -234,6 +234,11 @@ public class Rpc {
return transport.callForResult(new TypeReference<java.util.Map<String, String>>(){}, "batch_get_config", mapper.valueToTree(accountId), mapper.valueToTree(keys));
}
/** Returns all `ui.*` config keys that were set by the UI. */
public java.util.List<String> getAllUiConfigKeys(Integer accountId) throws RpcException {
return transport.callForResult(new TypeReference<java.util.List<String>>(){}, "get_all_ui_config_keys", mapper.valueToTree(accountId));
}
public void setStockStrings(java.util.Map<String, String> strings) throws RpcException {
transport.call("set_stock_strings", mapper.valueToTree(strings));
}
@@ -449,11 +454,11 @@ public class Rpc {
* Delete a chat.
* <p>
* Messages are deleted from the device and the chat database entry is deleted.
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
* After that, a `MsgsChanged` event is emitted.
* Messages are deleted from the server in background.
* <p>
* Things that are _not done_ implicitly:
* <p>
* - Messages are **not deleted from the server**.
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
* and the user may create the chat again.
* - **Groups are not left** - this would
@@ -677,7 +682,8 @@ public class Rpc {
* Set group name.
* <p>
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
* or if this is a brodacast channel,
* all members are informed by a special status message that is sent automatically by this function.
* <p>
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
@@ -685,11 +691,37 @@ public class Rpc {
transport.call("set_chat_name", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(newName));
}
/**
* Set group or broadcast channel description.
* <p>
* If the group is already _promoted_ (any message was sent to the group),
* or if this is a brodacast channel,
* all members are informed by a special status message that is sent automatically by this function.
* <p>
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
* <p>
* See also [`Self::get_chat_description`] / `getChatDescription()`.
*/
public void setChatDescription(Integer accountId, Integer chatId, String description) throws RpcException {
transport.call("set_chat_description", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(description));
}
/**
* Load the chat description from the database.
* <p>
* UIs show this in the profile page of the chat,
* it is settable by [`Self::set_chat_description`] / `setChatDescription()`.
*/
public String getChatDescription(Integer accountId, Integer chatId) throws RpcException {
return transport.callForResult(new TypeReference<String>(){}, "get_chat_description", mapper.valueToTree(accountId), mapper.valueToTree(chatId));
}
/**
* Set group profile image.
* <p>
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
* or if this is a brodacast channel,
* all members are informed by a special status message that is sent automatically by this function.
* <p>
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
* <p>
@@ -733,11 +765,26 @@ public class Rpc {
return transport.callForResult(new TypeReference<Integer>(){}, "add_device_message", mapper.valueToTree(accountId), mapper.valueToTree(label), mapper.valueToTree(msg));
}
/**
* Mark all messages in all chats as _noticed_.
* Skips messages from blocked contacts, but does not skip messages in muted chats.
* <p>
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
* but are still waiting for being marked as "seen" using markseen_msgs()
* (read receipts aren't sent for noticed messages).
* <p>
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also markseen_msgs().
*/
public void marknoticedAllChats(Integer accountId) throws RpcException {
transport.call("marknoticed_all_chats", mapper.valueToTree(accountId));
}
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
* but are still waiting for being marked as "seen" using markseen_msgs()
* (IMAP/MDNs is not done for noticed messages).
* (read receipts aren't sent for noticed messages).
* <p>
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also markseen_msgs().
@@ -893,6 +940,15 @@ public class Rpc {
return transport.callForResult(new TypeReference<MessageInfo>(){}, "get_message_info_object", mapper.valueToTree(accountId), mapper.valueToTree(messageId));
}
/**
* Returns count of read receipts on message.
* <p>
* This view count is meant as a feedback measure for the channel owner only.
*/
public Integer getMessageReadReceiptCount(Integer accountId, Integer messageId) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "get_message_read_receipt_count", mapper.valueToTree(accountId), mapper.valueToTree(messageId));
}
/** Returns contacts that sent read receipts and the time of reading. */
public java.util.List<MessageReadReceipt> getMessageReadReceipts(Integer accountId, Integer messageId) throws RpcException {
return transport.callForResult(new TypeReference<java.util.List<MessageReadReceipt>>(){}, "get_message_read_receipts", mapper.valueToTree(accountId), mapper.valueToTree(messageId));
@@ -1286,8 +1342,8 @@ public class Rpc {
}
/** Starts an outgoing call. */
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo));
public Integer placeOutgoingCall(Integer accountId, Integer chatId, String placeCallInfo, Boolean hasVideo) throws RpcException {
return transport.callForResult(new TypeReference<Integer>(){}, "place_outgoing_call", mapper.valueToTree(accountId), mapper.valueToTree(chatId), mapper.valueToTree(placeCallInfo), mapper.valueToTree(hasVideo));
}
/** Accepts an incoming call. */
@@ -2,7 +2,7 @@
package chat.delta.rpc.types;
/**
* cheaper version of fullchat, omits: - contacts - contact_ids - fresh_message_counter - ephemeral_timer - self_in_group - was_seen_recently - can_send
* cheaper version of fullchat, omits: - contact_ids - fresh_message_counter - ephemeral_timer - self_in_group - was_seen_recently - can_send
* <p>
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
@@ -2,7 +2,7 @@
package chat.delta.rpc.types;
public class CallInfo {
/** True if SDP offer has a video. */
/** True if the call is started as a video call. */
public Boolean hasVideo;
/**
* SDP offer.
@@ -38,7 +38,7 @@ public class Contact {
* <p>
* UI should display the information in the contact's profile as follows:
* <p>
* - If `verifierId` != 0, display text "Introduced by ..." with the name and address of the contact formatted by `name_and_addr`/`nameAndAddr`. Prefix the text by a green checkmark.
* - If `verifierId` != 0, display text "Introduced by ..." with the name of the contact. Prefix the text by a green checkmark.
* <p>
* - If `verifierId` == 0 and `isVerified` != 0, display "Introduced" prefixed by a green checkmark.
* <p>
@@ -412,7 +412,9 @@ public abstract class EventType {
/**
* One or more transports has changed.
* <p>
* This event is used for tests to detect when transport synchronization messages arrives. UIs don't need to use it, it is unlikely that user modifies transports on multiple devices simultaneously.
* UI should update the list.
* <p>
* This event is emitted when transport synchronization messages arrives, but not when the UI modifies the transport list by itself.
*/
public static class TransportsModified extends EventType {
}
@@ -7,7 +7,6 @@ public class FullChat {
public ChatType chatType;
public String color;
public java.util.List<Integer> contactIds;
public java.util.List<Contact> contacts;
public Integer ephemeralTimer;
public Integer freshMessageCounter;
public Integer id;
@@ -12,6 +12,7 @@ public class Message {
public String error;
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String file;
/** The size of the file in bytes, if applicable. If message is a pre-message, then this is the size of the file to be downloaded. */
public Integer fileBytes;
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String fileMime;
@@ -22,6 +22,7 @@ public abstract class MessageLoadResult {
public String error;
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String file;
/** The size of the file in bytes, if applicable. If message is a pre-message, then this is the size of the file to be downloaded. */
public Integer fileBytes;
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SET)
public String fileMime;
@@ -4,6 +4,7 @@ package chat.delta.rpc.types;
public enum SystemMessageType {
Unknown,
GroupNameChanged,
GroupDescriptionChanged,
GroupImageChanged,
MemberAddedToGroup,
MemberRemovedFromGroup,
@@ -2,8 +2,9 @@ package com.b44t.messenger;
public class DcAccounts {
public DcAccounts(String dir) {
accountsCPtr = createAccountsCPtr(dir);
public DcAccounts(String dir, DcEventChannel channel) {
accountsCPtr = createAccountsCPtr(dir, channel);
if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer");
}
@Override
@@ -45,7 +46,7 @@ public class DcAccounts {
// working with raw c-data
private long accountsCPtr; // CAVE: the name is referenced in the JNI
private native long createAccountsCPtr (String dir);
private native long createAccountsCPtr (String dir, DcEventChannel channel);
private native void unrefAccountsCPtr ();
private native long getEventEmitterCPtr ();
private native long getJsonrpcInstanceCPtr ();
@@ -0,0 +1,27 @@
package com.b44t.messenger;
public class DcEventChannel {
public DcEventChannel() {
eventChannelCPtr = createEventChannelCPtr();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (eventChannelCPtr != 0) {
unrefEventChannelCPtr();
eventChannelCPtr = 0;
}
}
public DcEventEmitter getEventEmitter() {
return new DcEventEmitter(getEventEmitterCPtr());
}
// working with raw c-data
private long eventChannelCPtr; // CAVE: the name is referenced in the JNI
private native long createEventChannelCPtr ();
private native void unrefEventChannelCPtr ();
private native long getEventEmitterCPtr ();
}
@@ -1,16 +1,24 @@
package org.thoughtcrime.securesms;
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;
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.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import androidx.viewpager.widget.ViewPager;
import com.b44t.messenger.DcChat;
@@ -18,11 +26,13 @@ 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.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
@@ -30,6 +40,7 @@ import java.util.ArrayList;
public class AllMediaActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate
{
private static final String TAG = AllMediaActivity.class.getSimpleName();
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
@@ -57,6 +68,10 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
private TabLayout tabLayout;
private ViewPager viewPager;
private @Nullable MediaController mediaController;
private ListenableFuture<MediaController> mediaControllerFuture;
private AudioPlaybackViewModel playbackViewModel;
@Override
protected void onPreCreate() {
dynamicTheme = new DynamicNoActionBarTheme();
@@ -91,11 +106,19 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
DcEventCenter eventCenter = DcHelper.getEventCenter(this);
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
initializeMediaController();
}
@Override
public void onDestroy() {
DcHelper.getEventCenter(this).removeObservers(this);
if (mediaController != null) {
MediaController.releaseFuture(mediaControllerFuture);
mediaController = null;
playbackViewModel.setMediaController(null);
}
super.onDestroy();
}
@@ -124,6 +147,40 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
}
private void initializeMediaController() {
SessionToken sessionToken = new SessionToken(this,
new ComponentName(this, AudioPlaybackService.class));
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
.buildAsync();
mediaControllerFuture.addListener(() -> {
try {
mediaController = mediaControllerFuture.get();
addActivityContext(
this.getIntent().getExtras(),
this.getClass().getName()
);
playbackViewModel.setMediaController(mediaController);
} catch (Exception e) {
Log.e(TAG, "Error connecting to audio playback service", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void addActivityContext(Bundle extras, String activityClassName) {
if (mediaController == null) return;
Bundle commandArgs = new Bundle();
commandArgs.putString("activity_class", activityClassName);
if (extras != null) {
commandArgs.putAll(extras);
}
SessionCommand updateContextCommand =
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
}
private boolean isGlobalGallery() {
return contactId==0 && chatId==0;
}
@@ -11,9 +11,10 @@ import androidx.annotation.NonNull;
import com.b44t.messenger.DcMsg;
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
@@ -31,7 +32,8 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
private final ItemClickListener itemClickListener;
private final Set<DcMsg> selected;
private BucketedThreadMedia media;
private BucketedThreadMedia media;
private AudioPlaybackViewModel playbackViewModel;
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
private final DocumentView documentView;
@@ -71,6 +73,10 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
this.media = media;
}
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
this.playbackViewModel = playbackViewModel;
}
@Override
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false));
@@ -97,7 +103,8 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
viewHolder.webxdcView.setVisibility(View.GONE);
viewHolder.audioView.setVisibility(View.VISIBLE);
viewHolder.audioView.setAudio((AudioSlide)slide, dcMsg.getDuration());
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
viewHolder.audioView.setAudio((AudioSlide)slide);
viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
viewHolder.audioView.disablePlayer(!selected.isEmpty());
@@ -16,6 +16,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.RecyclerView;
@@ -25,6 +26,7 @@ import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
@@ -54,7 +56,6 @@ public class AllMediaDocumentsFragment
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
dcContext = DcHelper.getContext(getContext());
chatId = getArguments().getInt(CHAT_ID_EXTRA, -1);
viewtype1 = getArguments().getInt(VIEWTYPE1, 0);
viewtype2 = getArguments().getInt(VIEWTYPE2, 0);
@@ -73,9 +74,11 @@ public class AllMediaDocumentsFragment
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
this.recyclerView.setAdapter(new AllMediaDocumentsAdapter(getContext(),
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
this));
AllMediaDocumentsAdapter adapter = new AllMediaDocumentsAdapter(getContext(),
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
this);
this.recyclerView.setAdapter(adapter);
adapter.setPlaybackViewModel(new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class));
this.recyclerView.setLayoutManager(gridManager);
this.recyclerView.setHasFixedSize(true);
@@ -240,12 +243,13 @@ public class AllMediaDocumentsFragment
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
int itemId = menuItem.getItemId();
AudioPlaybackViewModel playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
if (itemId == R.id.details) {
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedMedia()));
mode.finish();
return true;
} else if (itemId == R.id.delete) {
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia());
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
mode.finish();
return true;
} else if (itemId == R.id.share) {
@@ -52,7 +52,6 @@ public class AllMediaGalleryFragment
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
dcContext = DcHelper.getContext(getContext());
chatId = getArguments().getInt(CHAT_ID_EXTRA, -1);
getLoaderManager().initLoader(0, null, this);
@@ -23,6 +23,7 @@ import androidx.work.WorkManager;
import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcEventChannel;
import com.b44t.messenger.DcEventEmitter;
import com.b44t.messenger.FFITransport;
@@ -164,7 +165,8 @@ public class ApplicationContext extends MultiDexApplication {
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter, true));
String errorMsg = "Android " + Build.VERSION.RELEASE +":\n" + stringWriter.getBuffer().toString();
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + " Crash Report";
errorMsg += "\n" + LogViewFragment.grabLogcat();
String subject = "ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
@@ -195,7 +197,29 @@ public class ApplicationContext extends MultiDexApplication {
Util.runOnBackground(() -> {
synchronized (initLock) {
try {
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath());
DcEventChannel eventChannel = new DcEventChannel();
DcEventEmitter emitter = eventChannel.getEventEmitter();
eventCenter = new DcEventCenter(this);
new Thread(() -> {
Log.i(TAG, "Starting event loop");
while (true) {
DcEvent event = emitter.getNextEvent();
if (event == null) {
break;
}
if (isInitialized) {
eventCenter.handleEvent(event);
} else {
// not fully initialized, only handle logging events,
// ex. account migrations during DcAccounts initialization
eventCenter.handleLogging(event);
}
}
Log.i("DeltaChat", "shutting down event handler");
}, "eventThread").start();
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel);
Log.i(TAG, "DcAccounts created");
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
Log.i(TAG, "Rpc created");
@@ -235,28 +259,12 @@ public class ApplicationContext extends MultiDexApplication {
}
dcContext = dcAccounts.getSelectedAccount();
notificationCenter = new NotificationCenter(this);
eventCenter = new DcEventCenter(this);
dcLocationManager = new DcLocationManager(this, dcContext);
// Mark as initialized before starting threads that depend on it
isInitialized = true;
initLock.notifyAll();
Log.i(TAG, "DcAccounts initialization complete");
new Thread(() -> {
Log.i(TAG, "Starting event loop");
DcEventEmitter emitter = dcAccounts.getEventEmitter();
Log.i(TAG, "DcEventEmitter obtained");
while (true) {
DcEvent event = emitter.getNextEvent();
if (event==null) {
break;
}
eventCenter.handleEvent(event);
}
Log.i("DeltaChat", "shutting down event handler");
}, "eventThread").start();
// set translations before starting I/O to avoid sending untranslated MDNs (issue #2288)
DcHelper.setStockTranslations(this);
@@ -50,11 +50,11 @@ public abstract class BaseConversationItem extends LinearLayout
this.rpc = DcHelper.getRpc(context);
}
protected void bind(@NonNull DcMsg messageRecord,
@NonNull DcChat dcChat,
@NonNull Set<DcMsg> batchSelected,
boolean pulseHighlight,
@NonNull Recipient conversationRecipient)
protected void bindPartial(@NonNull DcMsg messageRecord,
@NonNull DcChat dcChat,
@NonNull Set<DcMsg> batchSelected,
boolean pulseHighlight,
@NonNull Recipient conversationRecipient)
{
this.messageRecord = messageRecord;
this.dcChat = dcChat;
@@ -126,6 +126,8 @@ public abstract class BaseConversationItem extends LinearLayout
public void onClick(View v) {
if (!shouldInterceptClicks(messageRecord) && parent != null) {
// The click workaround on ConversationItem shall be revised.
// In fact, it is probably better rethinking accessibility approach for the items.
if (batchSelected.isEmpty() && Util.isTouchExplorationEnabled(context)) {
BaseConversationItem.this.onAccessibilityClick();
}
@@ -6,6 +6,8 @@ import androidx.annotation.Nullable;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -17,7 +19,9 @@ public interface BindableConversationItem extends Unbindable {
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient recipients,
boolean pulseHighlight);
boolean pulseHighlight,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener);
DcMsg getMessageRecord();
@@ -34,6 +34,7 @@ import java.util.List;
public class ContactMultiSelectionActivity extends ContactSelectionActivity {
public static final String CONTACTS_EXTRA = "contacts_extra";
public static final String DESELECTED_CONTACTS_EXTRA = "deselected_contacts_extra";
@Override
protected void onCreate(Bundle icicle, boolean ready) {
@@ -71,7 +72,9 @@ public class ContactMultiSelectionActivity extends ContactSelectionActivity {
private void saveSelection() {
Intent resultIntent = getIntent();
List<Integer> selectedContacts = contactsFragment.getSelectedContacts();
List<Integer> deselectedContacts = contactsFragment.getDeselectedContacts();
resultIntent.putIntegerArrayListExtra(CONTACTS_EXTRA, new ArrayList<>(selectedContacts));
resultIntent.putIntegerArrayListExtra(DESELECTED_CONTACTS_EXTRA, new ArrayList<>(deselectedContacts));
setResult(RESULT_OK, resultIntent);
}
}
@@ -81,8 +81,9 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: super.onBackPressed(); return true;
if (item.getItemId() == android.R.id.home) {
getOnBackPressedDispatcher().onBackPressed();
return true;
}
return false;
@@ -22,6 +22,7 @@ import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -29,6 +30,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
@@ -56,6 +58,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -81,9 +84,11 @@ public class ContactSelectionListFragment extends Fragment
private DcContext dcContext;
private Set<Integer> selectedContacts;
private Set<Integer> deselectedContacts;
private OnContactSelectedListener onContactSelectedListener;
private String cursorFilter;
private RecyclerView recyclerView;
private TextView emptyView;
private ActionMode actionMode;
private ActionMode.Callback actionModeCallback;
@@ -113,6 +118,7 @@ public class ContactSelectionListFragment extends Fragment
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
emptyView = ViewUtil.findById(view, android.R.id.empty);
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
@@ -123,8 +129,8 @@ public class ContactSelectionListFragment extends Fragment
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.contact_list, menu);
setCorrectMenuVisibility(menu);
actionMode.setTitle("1");
menu.findItem(R.id.menu_delete_selected).setVisible(!isMulti());
updateActionModeState(actionMode);
return true;
}
@@ -161,19 +167,16 @@ public class ContactSelectionListFragment extends Fragment
private void handleSelectAll() {
getContactSelectionListAdapter().selectAll();
updateActionModeTitle();
updateActionModeState(actionMode);
}
private void updateActionModeTitle() {
actionMode.setTitle(String.valueOf(getContactSelectionListAdapter().getActionModeSelection().size()));
}
private void setCorrectMenuVisibility(Menu menu) {
ContactSelectionListAdapter adapter = getContactSelectionListAdapter();
if (adapter.getActionModeSelection().size() > 1) {
menu.findItem(R.id.menu_view_profile).setVisible(false);
private void updateActionModeState(ActionMode actionMode) {
int size = getContactSelectionListAdapter().getActionModeSelection().size();
if (size == 0) {
actionMode.finish();
} else {
menu.findItem(R.id.menu_view_profile).setVisible(true);
actionMode.getMenu().findItem(R.id.menu_view_profile).setVisible(size == 1);
actionMode.setTitle(String.valueOf(size));
}
}
@@ -226,6 +229,15 @@ public class ContactSelectionListFragment extends Fragment
return selected;
}
public @NonNull List<Integer> getDeselectedContacts() {
List<Integer> deselected = new LinkedList<>();
if (deselectedContacts != null) {
deselected.addAll(deselectedContacts);
}
return deselected;
}
private boolean isMulti() {
return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
}
@@ -241,6 +253,7 @@ public class ContactSelectionListFragment extends Fragment
isMulti(),
true);
selectedContacts = adapter.getSelectedContacts();
deselectedContacts = new HashSet<>();
ArrayList<Integer> preselectedContacts = getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS);
if(preselectedContacts!=null) {
selectedContacts.addAll(preselectedContacts);
@@ -266,7 +279,16 @@ public class ContactSelectionListFragment extends Fragment
@Override
public void onLoadFinished(Loader<DcContactsLoader.Ret> loader, DcContactsLoader.Ret data) {
((ContactSelectionListAdapter) recyclerView.getAdapter()).changeData(data);
ContactSelectionListAdapter adapter = (ContactSelectionListAdapter) recyclerView.getAdapter();
adapter.changeData(data);
if (emptyView != null) {
if (adapter.getItemCount() > 0 || TextUtils.isEmpty(cursorFilter)) {
emptyView.setVisibility(View.GONE);
} else {
emptyView.setText(getString(R.string.search_no_result_for_x, cursorFilter));
emptyView.setVisibility(View.VISIBLE);
}
}
}
@Override
@@ -280,10 +302,7 @@ public class ContactSelectionListFragment extends Fragment
{
if (handleActionMode) {
if (actionMode != null) {
Menu menu = actionMode.getMenu();
setCorrectMenuVisibility(menu);
updateActionModeTitle();
finishActionModeIfSelectionIsEmpty();
updateActionModeState(actionMode);
}
return;
}
@@ -303,12 +322,14 @@ public class ContactSelectionListFragment extends Fragment
}
selectedContacts.add(contactId);
deselectedContacts.remove(contactId);
contact.setChecked(true);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(contactId);
}
} else {
selectedContacts.remove(contactId);
deselectedContacts.add(contactId);
contact.setChecked(false);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contactId);
@@ -321,17 +342,11 @@ public class ContactSelectionListFragment extends Fragment
if (actionMode == null) {
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
} else {
finishActionModeIfSelectionIsEmpty();
updateActionModeState(actionMode);
}
}
}
private void finishActionModeIfSelectionIsEmpty() {
if (getContactSelectionListAdapter().getActionModeSelection().size() == 0) {
actionMode.finish();
}
}
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
this.onContactSelectedListener = onContactSelectedListener;
}
@@ -355,6 +370,7 @@ public class ContactSelectionListFragment extends Fragment
int contactId = data.getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0);
if (contactId != 0) {
selectedContacts.add(contactId);
deselectedContacts.remove(contactId);
}
getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this);
}
@@ -25,6 +25,7 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
@@ -57,6 +58,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
@@ -64,7 +66,12 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
@@ -75,7 +82,7 @@ import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.calls.CallUtil;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
import org.thoughtcrime.securesms.components.ComposeText;
@@ -85,6 +92,8 @@ import org.thoughtcrime.securesms.components.InputPanel;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import org.thoughtcrime.securesms.components.ScaleStableImageView;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
@@ -103,19 +112,19 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ShareUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.guava.Optional;
import org.thoughtcrime.securesms.util.views.ProgressDialog;
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
import org.thoughtcrime.securesms.calls.CallUtil;
import java.io.File;
import java.util.ArrayList;
@@ -137,14 +146,13 @@ import chat.delta.util.SettableFuture;
*/
@SuppressLint("StaticFieldLeak")
public class ConversationActivity extends PassphraseRequiredActionBarActivity
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
SearchView.OnQueryTextListener,
DcEventCenter.DcEventDelegate,
OnKeyboardShownListener,
InputPanel.Listener,
InputPanel.MediaListener
{
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
SearchView.OnQueryTextListener,
DcEventCenter.DcEventDelegate,
OnKeyboardShownListener,
InputPanel.Listener,
InputPanel.MediaListener, AudioView.OnActionListener {
private static final String TAG = ConversationActivity.class.getSimpleName();
public static final String ACCOUNT_ID_EXTRA = "account_id";
@@ -184,10 +192,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private MediaKeyboard emojiPicker;
protected HidingLinearLayout quickAttachmentToggle;
private InputPanel inputPanel;
private @Nullable MediaController mediaController;
private com.google.common.util.concurrent.ListenableFuture<MediaController> mediaControllerFuture;
private AudioPlaybackViewModel playbackViewModel;
private ApplicationContext context;
private Recipient recipient;
private DcContext dcContext;
private Rpc rpc;
private DcChat dcChat = new DcChat(0, 0);
private int chatId;
@@ -196,11 +206,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private boolean isSecurityInitialized = false;
private boolean successfulForwardingAttempt = false;
private boolean isEditing = false;
private boolean switchedProfile = false;
@Override
protected void onCreate(Bundle state, boolean ready) {
this.context = ApplicationContext.getInstance(getApplicationContext());
this.dcContext = DcHelper.getContext(context);
this.rpc = DcHelper.getRpc(context);
supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY);
@@ -217,6 +227,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeActionBar();
initializeViews();
initializeResources();
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
initializeMediaController();
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
@@ -237,17 +251,64 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
});
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (container.isInputOpen()) {
container.hideCurrentInput(composeText);
} else {
handleReturnToConversationList();
}
}
});
setDcEventListener();
handleRelaying();
}
private void setDcEventListener() {
DcEventCenter eventCenter = DcHelper.getEventCenter(this);
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
// first cleanup in case it was already registered for other chat
eventCenter.removeObservers(this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
if (!isMultiUser()) {
eventCenter.addObserver(DcContext.DC_EVENT_INCOMING_MSG, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_READ, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_INCOMING_MSG, this);
eventCenter.addMultiAccountObserver(DcContext.DC_EVENT_MSG_READ, this);
}
}
private void initializeMediaController() {
SessionToken sessionToken = new SessionToken(this,
new ComponentName(this, AudioPlaybackService.class));
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
.buildAsync();
mediaControllerFuture.addListener(() -> {
try {
mediaController = mediaControllerFuture.get();
playbackViewModel.setMediaController(mediaController);
} catch (Exception e) {
Log.e(TAG, "Error connecting to audio playback service", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void addActivityContext(Bundle extras, String activityClassName) {
if (mediaController == null) return;
Bundle commandArgs = new Bundle();
commandArgs.putString("activity_class", activityClassName);
if (extras != null) {
commandArgs.putAll(extras);
}
handleRelaying();
SessionCommand updateContextCommand =
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
}
@Override
@@ -273,6 +334,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
});
setDcEventListener(); // reset event listener
handleRelaying();
if (fragment != null) {
@@ -299,7 +361,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
titleView.setTitle(glideRequests, dcChat);
DcHelper.getNotificationCenter(this).updateVisibleChat(dcContext.getAccountId(), chatId);
try {
int accId = rpc.getSelectedAccountId();
DcHelper.getNotificationCenter(this).updateVisibleChat(accId, chatId);
} catch (RpcException e) {
Log.e(TAG, "rpc.getSelectedAccountId() failed", e);
}
attachmentManager.onResume();
}
@@ -313,7 +381,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DcHelper.getNotificationCenter(this).clearVisibleChat();
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
inputPanel.onPause();
AudioSlidePlayer.stopAll();
}
@Override
@@ -333,6 +400,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected void onDestroy() {
DcHelper.getEventCenter(this).removeObservers(this);
if (mediaController != null) {
MediaController.releaseFuture(mediaControllerFuture);
mediaController = null;
playbackViewModel.setMediaController(null);
}
super.onDestroy();
}
@@ -389,7 +461,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
break;
case GROUP_EDIT:
dcChat = dcContext.getChat(chatId);
dcChat = DcHelper.getContext(context).getChat(chatId);
titleView.setTitle(glideRequests, dcChat);
break;
@@ -545,8 +617,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else if (itemId == R.id.menu_show_map) {
WebxdcActivity.openMaps(this, chatId);
return true;
} else if (itemId == R.id.menu_start_call) {
CallUtil.startCall(this, chatId);
} else if (itemId == R.id.menu_start_audio_call) {
CallUtil.startCall(this, chatId, false);
return true;
} else if (itemId == R.id.menu_start_video_call) {
CallUtil.startCall(this, chatId, true);
return true;
} else if (itemId == R.id.menu_all_media) {
handleAllMedia();
@@ -568,15 +643,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return false;
}
@Override
public void onBackPressed() {
if (container.isInputOpen()){
container.hideCurrentInput(composeText);
} else {
handleReturnToConversationList();
}
}
@Override
public void onKeyboardShown() {
inputPanel.onKeyboardShown();
@@ -599,6 +665,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
//////// Event Handlers
private void handleEphemeralMessages() {
DcContext dcContext = DcHelper.getContext(context);
int preselected = dcContext.getChatEphemeralTimer(chatId);
EphemeralMessagesDialog.show(this, preselected, duration -> {
dcContext.setChatEphemeralTimer(chatId, (int) duration);
@@ -610,6 +677,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleReturnToConversationList(@Nullable Bundle extras) {
if (switchedProfile) { // force refreshing of chatlist
if (extras == null) extras = new Bundle();
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
}
playbackViewModel.stopNonMessageAudioPlayback();
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class));
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -619,6 +693,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleMuteNotifications() {
DcContext dcContext = DcHelper.getContext(context);
if(!dcChat.isMuted()) {
MuteDialog.show(this, duration -> {
dcContext.setChatMuteDuration(chatId, duration);
@@ -654,7 +729,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
AlertDialog dialog = new AlertDialog.Builder(this)
.setMessage(getString(R.string.ask_leave_group))
.setPositiveButton(leaveLabel, (d, which) -> {
dcContext.removeContactFromChat(chatId, DcContact.DC_CONTACT_ID_SELF);
DcHelper.getContext(context).removeContactFromChat(chatId, DcContact.DC_CONTACT_ID_SELF);
Toast.makeText(this, getString(R.string.done), Toast.LENGTH_SHORT).show();
})
.setNegativeButton(R.string.cancel, null)
@@ -663,6 +738,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleArchiveChat() {
DcContext dcContext = DcHelper.getContext(context);
int newVisibility = isArchived() ?
DcChat.DC_CHAT_VISIBILITY_NORMAL : DcChat.DC_CHAT_VISIBILITY_ARCHIVED;
dcContext.setChatVisibility(chatId, newVisibility);
@@ -678,7 +754,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
AlertDialog dialog = new AlertDialog.Builder(this)
.setMessage(getResources().getString(R.string.ask_delete_named_chat, dcChat.getName()))
.setPositiveButton(R.string.delete, (d, which) -> {
dcContext.deleteChat(chatId);
DcHelper.getContext(context).deleteChat(chatId);
DirectShareUtil.clearShortcut(this, chatId);
finish();
})
@@ -708,6 +784,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleForwarding() {
DcContext dcContext = DcHelper.getContext(context);
DcChat dcChat = dcContext.getChat(chatId);
if (dcChat.isSelfTalk()) {
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
@@ -756,7 +833,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
} else {
Uri uri = uriList.isEmpty()? null : uriList.get(0);
dcContext.setDraft(chatId, SendRelayedMessageUtil.createMessage(this, uri, ShareUtil.getSharedType(this), null, null, getSharedText(this)));
DcHelper.getContext(context).setDraft(chatId, SendRelayedMessageUtil.createMessage(this, uri, ShareUtil.getSharedType(this), null, null, getSharedText(this)));
}
initializeDraft();
}
@@ -771,7 +848,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private ListenableFuture<Boolean> initializeDraft() {
isEditing = false;
final SettableFuture<Boolean> future = new SettableFuture<>();
DcMsg draft = dcContext.getDraft(chatId);
DcMsg draft = DcHelper.getContext(context).getDraft(chatId);
final String sharedText = ShareUtil.getSharedText(this);
if (!draft.isOk()) {
@@ -932,7 +1009,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void initializeBackground() {
String backgroundImagePath = Prefs.getBackgroundImagePath(this, dcContext.getAccountId());
int accId;
try {
accId = rpc.getSelectedAccountId();
} catch (RpcException e) {
Log.e(TAG, "rpc.getSelectedAccountId() failed", e);
return;
}
String backgroundImagePath = Prefs.getBackgroundImagePath(this, accId);
Drawable background;
if(!backgroundImagePath.isEmpty()) {
background = Drawable.createFromPath(backgroundImagePath);
@@ -958,33 +1043,44 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void initializeResources() {
int accountId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, dcContext.getAccountId());
if (accountId != dcContext.getAccountId()) {
int selectedAccId = DcHelper.getContext(context).getAccountId();
int accountId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, selectedAccId);
if (accountId != selectedAccId) {
switchedProfile = true;
AccountManager.getInstance().switchAccount(context, accountId);
fragment.dcContext = dcContext = context.getDcContext();
initializeBackground();
}
chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, -1);
if(chatId == DcChat.DC_CHAT_NO_CHAT)
throw new IllegalStateException("can't display a conversation for no chat.");
dcChat = dcContext.getChat(chatId);
dcChat = DcHelper.getContext(context).getChat(chatId);
recipient = new Recipient(this, dcChat);
glideRequests = GlideApp.with(this);
setComposePanelVisibility();
setComposePanelVisibility(true);
initializeContactRequest();
}
private void setComposePanelVisibility() {
private void setComposePanelVisibility(boolean isInitialization) {
if (dcChat.canSend()) {
composePanel.setVisibility(View.VISIBLE);
attachmentManager.setHidden(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);
hideSoftKeyboard();
inputPanel.setSubjectVisible(false);
// FIXME: disabled for now to avoid problems with chat scrolling and keyboard covering input bar
/*
if (isInitialization) {
ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
fragment.handleAddBottomInsets();
}
*/
}
}
@@ -1021,11 +1117,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return new SettableFuture<>(false);
}
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId);
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId, playbackViewModel);
}
private ListenableFuture<Boolean> setMedia(DcMsg msg, @NonNull MediaType mediaType) {
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId);
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId, playbackViewModel);
}
private void addAttachmentContactInfo(int contactId) {
@@ -1034,11 +1130,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
try {
byte[] vcard = rpc.makeVcard(dcContext.getAccountId(), Collections.singletonList(contactId)).getBytes();
byte[] vcard = rpc.makeVcard(rpc.getSelectedAccountId(), Collections.singletonList(contactId)).getBytes();
String mimeType = "application/octet-stream";
setMedia(PersistentBlobProvider.getInstance().create(this, vcard, mimeType, "vcard.vcf"), MediaType.DOCUMENT);
} catch (RpcException e) {
Log.e(TAG, "makeVcard() failed", e);
Log.e(TAG, "RPC failed", e);
}
}
@@ -1077,6 +1173,11 @@ 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();
DcContext dcContext = DcHelper.getContext(context);
Util.runOnAnyBackgroundThread(() -> {
DcMsg msg = null;
int recompress = 0;
@@ -1209,7 +1310,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (refreshFragment) {
fragment.reload(recipient, chatId);
DcHelper.getNotificationCenter(this).updateVisibleChat(dcContext.getAccountId(), chatId);
try {
int accId = rpc.getSelectedAccountId();
DcHelper.getNotificationCenter(this).updateVisibleChat(accId, chatId);
} catch (RpcException e) {
Log.e(TAG, "rpc.getSelectedAccountId() failed", e);
}
}
fragment.scrollToBottom();
@@ -1371,6 +1477,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Optional<QuoteModel> quote = inputPanel.getQuote();
inputPanel.clearQuote();
DcContext dcContext = DcHelper.getContext(context);
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_STICKER);
if (quote.isPresent()) {
msg.setQuote(quote.get().getQuotedMsg());
@@ -1382,6 +1489,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// Listeners
@Override
public void onPlayPauseButtonClicked(View view) {
addActivityContext(
this.getIntent().getExtras(),
this.getClass().getName()
);
}
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
@Override
public void onClick(int type) {
@@ -1492,7 +1607,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (isEditing) composeText.setText("");
isEditing = false;
// If you modify these lines you may also want to modify ConversationItem.setQuote():
Recipient author = new Recipient(this, dcContext.getContact(msg.getFromId()));
Recipient author = new Recipient(this, DcHelper.getContext(context).getContact(msg.getFromId()));
SlideDeck slideDeck = new SlideDeck();
if (msg.hasFile()) {
@@ -1515,7 +1630,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void handleEditMessage(DcMsg msg) {
isEditing = true;
Recipient author = new Recipient(this, dcContext.getContact(msg.getFromId()));
Recipient author = new Recipient(this, DcHelper.getContext(context).getContact(msg.getFromId()));
SlideDeck slideDeck = new SlideDeck();
String text = msg.getSummarytext(500);
@@ -1540,6 +1655,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void handleEvent(@NonNull DcEvent event) {
DcContext dcContext = DcHelper.getContext(context);
if (event.getAccountId() != dcContext.getAccountId()) return;
int eventId = event.getId();
if ((eventId == DcContext.DC_EVENT_CHAT_MODIFIED && event.getData1Int() == chatId)
|| (eventId == DcContext.DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED && event.getData1Int() == chatId)
@@ -1547,7 +1665,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
dcChat = dcContext.getChat(chatId);
titleView.setTitle(glideRequests, dcChat);
initializeSecurity(isSecureText, isDefaultSms);
setComposePanelVisibility();
setComposePanelVisibility(false);
initializeContactRequest();
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|| eventId == DcContext.DC_EVENT_MSG_READ)
@@ -1626,7 +1744,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
String normQuery = query.trim();
searchResult = dcContext.searchMsgs(chatId, normQuery);
searchResult = DcHelper.getContext(context).searchMsgs(chatId, normQuery);
if(searchResult.length>0) {
searchResultPosition = searchResult.length - 1;
@@ -1657,7 +1775,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
messageRequestBottomView.setVisibility(View.VISIBLE);
messageRequestBottomView.setAcceptOnClickListener(v -> {
dcContext.acceptChat(chatId);
DcHelper.getContext(context).acceptChat(chatId);
messageRequestBottomView.setVisibility(View.GONE);
composePanel.setVisibility(View.VISIBLE);
});
@@ -1675,7 +1793,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// avoid showing compose panel on receiving DC_EVENT_CONTACTS_CHANGED for the chat that is no longer a request after blocking
DcHelper.getEventCenter(this).removeObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
dcContext.blockChat(chatId);
DcHelper.getContext(context).blockChat(chatId);
Bundle extras = new Bundle();
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
handleReturnToConversationList(extras);
@@ -34,6 +34,8 @@ import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -59,6 +61,7 @@ import java.util.Set;
* @author Moxie Marlinspike
*
*/
// FIXME: this breaks type checks, that is why there are so many casts.
public class ConversationAdapter <V extends View & BindableConversationItem>
extends RecyclerView.Adapter
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
@@ -97,6 +100,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private long pulseHighlightingSince = -1;
private int lastSeenPosition = -1;
private long lastSeen = -1;
private AudioPlaybackViewModel playbackViewModel;
private AudioView.OnActionListener audioPlayPauseListener;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
@@ -170,6 +175,14 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
return fromDb;
}
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
this.playbackViewModel = playbackViewModel;
}
public void setAudioPlayPauseListener(AudioView.OnActionListener audioPlayPauseListener) {
this.audioPlayPauseListener = audioPlayPauseListener;
}
/**
* Returns the position of the message with msgId in the chat list, counted from the top
*/
@@ -237,7 +250,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
long elapsed = now - pulseHighlightingSince;
boolean pulseHighlight = (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS);
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight);
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight, playbackViewModel, audioPlayPauseListener);
}
@Override
@@ -41,6 +41,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
@@ -52,6 +53,7 @@ import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -68,6 +70,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -75,6 +78,9 @@ import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends MessageSelectorFragment
{
@@ -97,17 +103,21 @@ public class ConversationFragment extends MessageSelectorFragment
private StickyHeaderDecoration dateDecoration;
private View scrollToBottomButton;
private View floatingLocationButton;
private View bottomDivider;
private AddReactionView addReactionView;
private TextView noMessageTextView;
private Timer reloadTimer;
public boolean isPaused;
private Debouncer markseenDebouncer;
private Rpc rpc;
private boolean pendingAddBottomInsets;
private boolean pendingRemoveBottomInsets;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
this.dcContext = DcHelper.getContext(getContext());
rpc = DcHelper.getRpc(getContext());
DcEventCenter eventCenter = DcHelper.getEventCenter(getContext());
eventCenter.addObserver(DcContext.DC_EVENT_INCOMING_MSG, this);
@@ -136,6 +146,7 @@ public class ConversationFragment extends MessageSelectorFragment
floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button);
addReactionView = ViewUtil.findById(view, R.id.add_reaction_view);
noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view);
bottomDivider = ViewUtil.findById(view, R.id.bottom_divider);
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
@@ -154,18 +165,32 @@ public class ConversationFragment extends MessageSelectorFragment
// with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture"
list.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
if (pendingAddBottomInsets) {
bottomDivider.setVisibility(View.GONE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true);
pendingAddBottomInsets = false;
}
if (pendingRemoveBottomInsets) {
bottomDivider.setVisibility(View.VISIBLE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false);
pendingRemoveBottomInsets = false;
}
return view;
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initializeResources();
initializeListAdapter();
}
private void setNoMessageText() {
private void setNoMessageText() {
DcChat dcChat = getListAdapter().getChat();
if(dcChat.isMultiUser()){
if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) {
@@ -192,6 +217,28 @@ public class ConversationFragment extends MessageSelectorFragment
}
}
public void handleAddBottomInsets() {
if (bottomDivider != null) {
bottomDivider.setVisibility(View.GONE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true);
pendingAddBottomInsets = false;
} else {
pendingAddBottomInsets = true;
}
}
public void handleRemoveBottomInsets() {
if (bottomDivider != null) {
bottomDivider.setVisibility(View.VISIBLE);
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false);
pendingRemoveBottomInsets = false;
} else {
pendingRemoveBottomInsets = true;
}
}
@Override
public void onDestroy() {
DcHelper.getEventCenter(getContext()).removeObservers(this);
@@ -208,8 +255,13 @@ public class ConversationFragment extends MessageSelectorFragment
@Override
public void onResume() {
super.onResume();
Util.runOnBackground(() -> dcContext.marknoticedChat((int) chatId));
Util.runOnBackground(() -> {
try {
rpc.marknoticedChat(rpc.getSelectedAccountId(), (int) chatId);
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
});
if (list.getAdapter() != null) {
list.getAdapter().notifyDataSetChanged();
}
@@ -281,6 +333,10 @@ public class ConversationFragment extends MessageSelectorFragment
if (this.recipient != null && this.chatId != -1) {
ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), selectionClickListener, this.recipient);
list.setAdapter(adapter);
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
adapter.setPlaybackViewModel(playbackViewModel);
adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity()));
if (dateDecoration != null) {
list.removeItemDecoration(dateDecoration);
@@ -288,7 +344,12 @@ public class ConversationFragment extends MessageSelectorFragment
dateDecoration = new StickyHeaderDecoration(adapter, false, false);
list.addItemDecoration(dateDecoration);
int freshMsgs = dcContext.getFreshMsgCount((int) chatId);
int freshMsgs = 0;
try {
freshMsgs = rpc.getFreshMsgCnt(rpc.getSelectedAccountId(), (int) chatId);
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
SetStartingPositionLinearLayoutManager layoutManager = (SetStartingPositionLinearLayoutManager) list.getLayoutManager();
if (startingPosition > -1) {
layoutManager.setStartingPosition(startingPosition);
@@ -392,7 +453,14 @@ public class ConversationFragment extends MessageSelectorFragment
}
public void handleClearChat() {
handleDeleteMessages((int) chatId, getListAdapter().getMessageIds());
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
handleDeleteMessages(
(int) chatId,
getListAdapter().getMessageIds(),
playbackViewModel::stopByIds,
playbackViewModel::stopByIds);
}
private ConversationAdapter getListAdapter() {
@@ -448,6 +516,7 @@ public class ConversationFragment extends MessageSelectorFragment
StringBuilder result = new StringBuilder();
DcContext dcContext = DcHelper.getContext(getContext());
DcMsg prevMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
for (DcMsg msg : dcMsgsList) {
if (result.length() > 0) {
@@ -476,9 +545,13 @@ public class ConversationFragment extends MessageSelectorFragment
private void handleForwardMessage(final Set<DcMsg> messageRecords) {
Intent composeIntent = new Intent();
int[] msgIds = DcMsg.msgSetToIds(messageRecords);
setForwardingMessageIds(composeIntent, msgIds);
ConversationListRelayingActivity.start(this, composeIntent);
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
try {
setForwardingMessageIds(composeIntent, msgIds, rpc.getSelectedAccountId());
ConversationListRelayingActivity.start(this, composeIntent);
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
}
@SuppressLint("RestrictedApi")
@@ -504,6 +577,7 @@ public class ConversationFragment extends MessageSelectorFragment
private void handleReplyMessagePrivately(final DcMsg msg) {
if (getActivity() != null) {
DcContext dcContext = DcHelper.getContext(getActivity());
int privateChatId = dcContext.createChatByContactId(msg.getFromId());
DcMsg replyMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
replyMsg.setQuote(msg);
@@ -520,10 +594,14 @@ public class ConversationFragment extends MessageSelectorFragment
private void handleToggleSave(final Set<DcMsg> messageRecords) {
DcMsg msg = getSelectedMessageRecord(messageRecords);
if (msg.getSavedMsgId() != 0) {
dcContext.deleteMsgs(new int[]{msg.getSavedMsgId()});
} else {
dcContext.saveMsgs(new int[]{msg.getId()});
try {
if (msg.getSavedMsgId() != 0) {
rpc.deleteMessages(rpc.getSelectedAccountId(), Collections.singletonList(msg.getSavedMsgId()));
} else {
rpc.saveMsgs(rpc.getSelectedAccountId(), Collections.singletonList(msg.getId()));
}
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
}
@@ -593,7 +671,7 @@ public class ConversationFragment extends MessageSelectorFragment
}
private void updateLocationButton() {
floatingLocationButton.setVisibility(dcContext.isSendingLocationsToChat((int) chatId)? View.VISIBLE : View.GONE);
floatingLocationButton.setVisibility(DcHelper.getContext(getContext()).isSendingLocationsToChat((int) chatId)? View.VISIBLE : View.GONE);
}
private void scrollAndHighlight(final int pos, boolean smooth) {
@@ -729,16 +807,20 @@ public class ConversationFragment extends MessageSelectorFragment
return;
}
int[] ids = new int[lastPos - firstPos + 1];
int index = 0;
ArrayList<Integer> ids = new ArrayList<>(lastPos - firstPos + 1);
for(int pos = firstPos; pos <= lastPos; pos++) {
DcMsg message = ((ConversationAdapter) list.getAdapter()).getMsg(pos);
if (message.getFromId() != DC_CONTACT_ID_SELF) {
ids[index] = message.getId();
index++;
ids.add(message.getId());
}
}
Util.runOnAnyBackgroundThread(() -> dcContext.markseenMsgs(ids));
Util.runOnAnyBackgroundThread(() -> {
try {
rpc.markseenMsgs(rpc.getSelectedAccountId(), ids);
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
});
}
private class ConversationFragmentItemClickListener implements ItemClickListener {
@@ -783,7 +865,7 @@ public class ConversationFragment extends MessageSelectorFragment
startActivity(intent);
}
else {
String self_mail = dcContext.getConfig("configured_mail_user");
String self_mail = DcHelper.getContext(getContext()).getConfig("configured_mail_user");
if (self_mail != null && !self_mail.isEmpty()
&& messageRecord.getText().contains(self_mail)
&& getListAdapter().getChat().isDeviceTalk()) {
@@ -822,7 +904,7 @@ public class ConversationFragment extends MessageSelectorFragment
if (foreignChatId != 0 && foreignChatId != chatId) {
Intent intent = new Intent(getActivity(), ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, foreignChatId);
int start = DcMsg.getMessagePosition(original, dcContext);
int start = DcMsg.getMessagePosition(original, DcHelper.getContext(getContext()));
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, start);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
((ConversationActivity) getActivity()).hideSoftKeyboard();
@@ -838,7 +920,7 @@ public class ConversationFragment extends MessageSelectorFragment
@Override
public void onJumpToOriginalClicked(DcMsg messageRecord) {
jumpToOriginal(dcContext.getMsg(messageRecord.getOriginalMsgId()));
jumpToOriginal(DcHelper.getContext(getContext()).getMsg(messageRecord.getOriginalMsgId()));
}
@Override
@@ -857,12 +939,12 @@ public class ConversationFragment extends MessageSelectorFragment
@Override
public void onDownloadClicked(DcMsg messageRecord) {
dcContext.downloadFullMsg(messageRecord.getId());
DcHelper.getContext(getContext()).downloadFullMsg(messageRecord.getId());
}
@Override
public void onReactionClicked(DcMsg messageRecord) {
ReactionsDetailsFragment dialog = new ReactionsDetailsFragment(messageRecord.getId());
ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId());
dialog.show(getActivity().getSupportFragmentManager(), null);
}
}
@@ -911,12 +993,15 @@ public class ConversationFragment extends MessageSelectorFragment
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
hideAddReactionView();
int itemId = item.getItemId();
AudioPlaybackViewModel playbackViewModel =
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
if (itemId == R.id.menu_context_copy) {
handleCopyMessage(getListAdapter().getSelectedItems());
actionMode.finish();
return true;
} else if (itemId == R.id.menu_context_delete_message) {
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems());
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
return true;
} else if (itemId == R.id.menu_context_share) {
DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND);
@@ -41,8 +41,7 @@ import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.calls.CallUtil;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.CallItemView;
@@ -52,6 +51,8 @@ import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.VcardView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
@@ -71,7 +72,6 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.calls.CallUtil;
import java.util.List;
import java.util.Set;
@@ -181,9 +181,11 @@ public class ConversationItem extends BaseConversationItem
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient recipients,
boolean pulseHighlight)
boolean pulseHighlight,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
{
bind(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
bindPartial(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
this.glideRequests = glideRequests;
this.showSender = ((dcChat.isMultiUser() || dcChat.isSelfTalk()) && !messageRecord.isOutgoing()) || messageRecord.getOverrideSenderName() != null;
@@ -204,7 +206,7 @@ public class ConversationItem extends BaseConversationItem
setGutterSizes(messageRecord, showSender);
setMessageShape(messageRecord);
setMediaAttributes(messageRecord, showSender);
setMediaAttributes(messageRecord, showSender, playbackViewModel, audioPlayPauseListener);
setBodyText(messageRecord);
setBubbleState(messageRecord);
setContactPhoto();
@@ -482,24 +484,10 @@ public class ConversationItem extends BaseConversationItem
}
private void setMediaAttributes(@NonNull DcMsg messageRecord,
boolean showSender)
boolean showSender,
AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
{
class SetDurationListener implements AudioSlidePlayer.Listener {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {
messageRecord.lateFilingMediaSize(0,0, millis);
audioViewStub.get().setDuration(millis);
}
}
if (hasAudio(messageRecord)) {
audioViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
@@ -509,15 +497,9 @@ public class ConversationItem extends BaseConversationItem
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
int duration = messageRecord.getDuration();
if (duration == 0) {
AudioSlide audio = new AudioSlide(context, messageRecord);
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, new SetDurationListener());
audioSlidePlayer.requestDuration();
}
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord), duration);
audioViewStub.get().setPlaybackViewModel(playbackViewModel);
audioViewStub.get().setOnActionListener(audioPlayPauseListener);
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord));
audioViewStub.get().setOnClickListener(passthroughClickListener);
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
audioViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
@@ -1000,9 +982,9 @@ public class ConversationItem extends BaseConversationItem
int chatId = messageRecord.getChatId();
if (!messageRecord.isOutgoing() && callInfo.state instanceof CallState.Alerting) {
int callId = messageRecord.getId();
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer);
CallUtil.openCall(getContext(), accId, chatId, callId, callInfo.sdpOffer, callInfo.hasVideo);
} else {
CallUtil.startCall(getContext(), accId, chatId);
CallUtil.startCall(getContext(), accId, chatId, callInfo.hasVideo);
}
}
}
@@ -16,8 +16,6 @@
*/
package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.ConversationActivity.CHAT_ID_EXTRA;
import static org.thoughtcrime.securesms.ConversationActivity.STARTING_POSITION_EXTRA;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_ENABLED;
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_URL;
import static org.thoughtcrime.securesms.util.ShareUtil.acquireRelayMessageContent;
@@ -25,10 +23,12 @@ import static org.thoughtcrime.securesms.util.ShareUtil.getDirectSharingChatId;
import static org.thoughtcrime.securesms.util.ShareUtil.getSharedTitle;
import static org.thoughtcrime.securesms.util.ShareUtil.isDirectSharing;
import static org.thoughtcrime.securesms.util.ShareUtil.isForwarding;
import static org.thoughtcrime.securesms.util.ShareUtil.getForwardedMessageAccountId;
import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent;
import static org.thoughtcrime.securesms.util.ShareUtil.resetRelayingMessageContent;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
@@ -46,6 +46,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
@@ -98,6 +99,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
public static final String CLEAR_NOTIFICATIONS = "clear_notifications";
public static final String ACCOUNT_ID_EXTRA = "account_id";
public static final String FROM_WELCOME = "from_welcome";
public static final String FROM_WELCOME_RAW_QR = "from_welcome_raw_qr";
private static final int REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE = ScreenLockUtil.REQUEST_CODE_CONFIRM_CREDENTIALS+1;
private ConversationListFragment conversationListFragment;
@@ -123,36 +125,9 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
@Override
protected void onCreate(Bundle icicle, boolean ready) {
// update messages - for new messages, do not reuse or modify strings but create new ones.
// it is not needed to keep all past update messages, however, when deleted, also the strings should be deleted.
try {
DcContext dcContext = DcHelper.getContext(this);
final String deviceMsgLabel = "update_2_33_1_android";
if (!dcContext.wasDeviceMsgEverAdded(deviceMsgLabel)) {
DcMsg msg = null;
if (!getIntent().getBooleanExtra(FROM_WELCOME, false)) {
msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
// InputStream inputStream = getResources().getAssets().open("device-messages/green-checkmark.jpg");
// String outputFile = DcHelper.getBlobdirFile(dcContext, "green-checkmark", ".jpg");
// Util.copy(inputStream, new FileOutputStream(outputFile));
// msg.setFile(outputFile, "image/jpeg");
msg.setText(getString(R.string.update_2_33, "https://arcanechat.me/#contribute"));
}
dcContext.addDeviceMsg(deviceMsgLabel, msg);
if (Prefs.getStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, "").equals(deviceMsgLabel)) {
int deviceChatId = dcContext.getChatIdByContactId(DcContact.DC_CONTACT_ID_DEVICE);
if (deviceChatId != 0) {
dcContext.marknoticedChat(deviceChatId);
}
}
Prefs.setStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, deviceMsgLabel);
}
} catch(Exception e) {
e.printStackTrace();
addDeviceMessages(getIntent().getBooleanExtra(FROM_WELCOME, false));
if (getIntent().getIntExtra(ACCOUNT_ID_EXTRA, -1) <= 0) {
getIntent().putExtra(ACCOUNT_ID_EXTRA, DcHelper.getContext(this).getAccountId());
}
// create view
@@ -176,13 +151,41 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
initializeSearchListener();
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (searchToolbar.isVisible()) {
searchToolbar.collapse();
} else {
Activity activity = ConversationListActivity.this;
if (isRelayingMessageContent(activity)) {
int selectedAccId = DcHelper.getContext(activity).getAccountId();
int initialAccId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, selectedAccId);
if (initialAccId != selectedAccId) {
// allowing to go back is dangerous, it could be activity on previously selected account,
// instead of figuring out account rollback in onResume in each activity (conversation, gallery, media preview, webxdc, etc.)
// just clear the back stack and stay in newly selected account
finishAffinity();
startActivity(new Intent(activity, ConversationListActivity.class));
return;
} else {
handleResetRelaying();
}
}
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
}
}
});
TooltipCompat.setTooltipText(searchAction, getText(R.string.search_explain));
TooltipCompat.setTooltipText(selfAvatar, getText(R.string.switch_account));
selfAvatar.setOnClickListener(v -> AccountManager.getInstance().showSwitchAccountMenu(this));
selfAvatar.setOnClickListener(v -> AccountManager.getInstance().showSwitchAccountMenu(this, false));
findViewById(R.id.avatar_and_title).setOnClickListener(v -> {
if (!isRelayingMessageContent(this)) {
AccountManager.getInstance().showSwitchAccountMenu(this);
AccountManager.getInstance().showSwitchAccountMenu(this, false);
}
});
@@ -191,6 +194,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
if (BuildConfig.DEBUG) checkNdkArchitecture();
DcHelper.maybeShowMigrationError(this);
String rawQrString = getIntent().getStringExtra(FROM_WELCOME_RAW_QR);
// Launch chat directly, if coming from onboarding with a join chat/group QR
if (rawQrString != null) {
QrCodeHandler qrCodeHandler = new QrCodeHandler(this);
qrCodeHandler.secureJoinByQr(rawQrString, SecurejoinSource.Scan, SecurejoinUiPath.Unknown);
}
}
/**
@@ -257,24 +267,29 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
}
super.onNewIntent(intent);
setIntent(intent);
if (getIntent().getIntExtra(ACCOUNT_ID_EXTRA, -1) <= 0) {
getIntent().putExtra(ACCOUNT_ID_EXTRA, DcHelper.getContext(this).getAccountId());
}
refresh();
conversationListFragment.onNewIntent();
invalidateOptionsMenu();
}
private void refresh() {
DcContext dcContext = DcHelper.getContext(this);
int accountId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, dcContext.getAccountId());
int selectedAccId = DcHelper.getContext(this).getAccountId();
int accountId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, selectedAccId);
if (getIntent().getBooleanExtra(CLEAR_NOTIFICATIONS, false)) {
DcHelper.getNotificationCenter(this).removeAllNotifications(accountId);
}
if (accountId != dcContext.getAccountId()) {
AccountManager.getInstance().switchAccountAndStartActivity(this, accountId);
if (accountId != selectedAccId) {
AccountManager.getInstance().switchAccount(this, accountId);
onProfileSwitched(accountId);
} else {
refreshAvatar();
refreshUnreadIndicator();
refreshTitle();
}
refreshAvatar();
refreshUnreadIndicator();
refreshTitle();
handleOpenpgp4fpr();
if (isDirectSharing(this)) {
openConversation(getDirectSharingChatId(this), -1);
@@ -452,13 +467,16 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
startActivity(new Intent(this, ProxySettingsActivity.class));
return true;
} else if (itemId == android.R.id.home) {
onBackPressed();
getOnBackPressedDispatcher().onBackPressed();
return true;
} else if (itemId == R.id.menu_all_media) {
startActivity(new Intent(this, AllMediaActivity.class));
return true;
} else if (itemId == R.id.menu_export_attachment) {
handleSaveAttachment();
} else if (itemId == R.id.menu_switch_account) {
AccountManager.getInstance().showSwitchAccountMenu(this, true);
return true;
}
return false;
@@ -491,7 +509,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
};
SaveAttachmentTask saveTask = new SaveAttachmentTask(this);
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments);
onBackPressed();
getOnBackPressedDispatcher().onBackPressed();
}
private void handleOpenpgp4fpr() {
@@ -526,15 +544,17 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
searchToolbar.clearFocus();
final DcContext dcContext = DcHelper.getContext(this);
if (isForwarding(this) && dcContext.getChat(chatId).isSelfTalk()) {
int fwdAccId = getForwardedMessageAccountId(this);
if (fwdAccId == dcContext.getAccountId() && dcContext.getChat(chatId).isSelfTalk()) {
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
Toast.makeText(this, DynamicTheme.getCheckmarkEmoji(this) + " " + getString(R.string.saved), Toast.LENGTH_SHORT).show();
handleResetRelaying();
finish();
} else {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(CHAT_ID_EXTRA, chatId);
intent.putExtra(STARTING_POSITION_EXTRA, startingPosition);
intent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, dcContext.getAccountId());
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
if (isRelayingMessageContent(this)) {
acquireRelayMessageContent(this, intent);
}
@@ -554,15 +574,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
}
@Override
public void onBackPressed() {
if (searchToolbar.isVisible()) searchToolbar.collapse();
else if (isRelayingMessageContent(this)) {
handleResetRelaying();
finish();
} else super.onBackPressed();
}
private void createChat() {
Intent intent = new Intent(this, NewConversationActivity.class);
if (isRelayingMessageContent(this)) {
@@ -579,6 +590,48 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
startActivity(Intent.createChooser(intent, getString(R.string.chat_share_with_title)));
}
private void addDeviceMessages(boolean fromWelcome) {
// update messages - for new messages, do not reuse or modify strings but create new ones.
// it is not needed to keep all past update messages, however, when deleted, also the strings should be deleted.
try {
DcContext dcContext = DcHelper.getContext(this);
final String deviceMsgLabel = "update_2_33_1_android";
if (!dcContext.wasDeviceMsgEverAdded(deviceMsgLabel)) {
DcMsg msg = null;
if (!getIntent().getBooleanExtra(FROM_WELCOME, false)) {
msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
// InputStream inputStream = getResources().getAssets().open("device-messages/green-checkmark.jpg");
// String outputFile = DcHelper.getBlobdirFile(dcContext, "green-checkmark", ".jpg");
// Util.copy(inputStream, new FileOutputStream(outputFile));
// msg.setFile(outputFile, "image/jpeg");
msg.setText(getString(R.string.update_2_33, "https://arcanechat.me/#contribute"));
}
dcContext.addDeviceMsg(deviceMsgLabel, msg);
if (Prefs.getStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, "").equals(deviceMsgLabel)) {
int deviceChatId = dcContext.getChatIdByContactId(DcContact.DC_CONTACT_ID_DEVICE);
if (deviceChatId != 0) {
dcContext.marknoticedChat(deviceChatId);
}
}
Prefs.setStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, deviceMsgLabel);
}
} catch(Exception e) {
e.printStackTrace();
}
}
public void onProfileSwitched(int profileId) {
addDeviceMessages(false);
refreshAvatar();
refreshUnreadIndicator();
refreshTitle();
conversationListFragment.loadChatlistAsync();
}
public void onDeleteProfile(int profileId) {
deleteProfileId = profileId;
boolean result = ScreenLockUtil.applyScreenLock(this, getString(R.string.delete_account), getString(R.string.enter_system_secret_to_continue), REQUEST_CODE_CONFIRM_CREDENTIALS_DELETE_PROFILE);
@@ -594,9 +647,14 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
accounts.removeAccount(profileId);
if (selected) {
DcContext selAcc = accounts.getSelectedAccount();
AccountManager.getInstance().switchAccountAndStartActivity(this, selAcc.isOk()? selAcc.getAccountId() : 0);
if (selAcc.isOk()) {
AccountManager.getInstance().switchAccount(this, selAcc.getAccountId());
onProfileSwitched(selAcc.getAccountId());
} else {
AccountManager.getInstance().switchAccountAndStartActivity(this, 0);
}
} else {
AccountManager.getInstance().showSwitchAccountMenu(this);
AccountManager.getInstance().showSwitchAccountMenu(this, false);
}
// title update needed to show "Delta Chat" in case there is only one profile left
@@ -12,6 +12,8 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.activity.OnBackPressedCallback;
import com.b44t.messenger.DcChat;
import org.thoughtcrime.securesms.connect.DcHelper;
@@ -33,6 +35,21 @@ public class ConversationListArchiveActivity extends PassphraseRequiredActionBar
Bundle bundle = new Bundle();
bundle.putBoolean(ConversationListFragment.ARCHIVE, true);
initFragment(R.id.fragment, new ConversationListFragment(), bundle);
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (!isRelayingMessageContent(ConversationListArchiveActivity.this)) {
// Load the ConversationListActivity in case it's not existent for some reason
Intent intent = new Intent(ConversationListArchiveActivity.this, ConversationListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
}
});
}
@Override
@@ -63,7 +80,7 @@ public class ConversationListArchiveActivity extends PassphraseRequiredActionBar
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
onBackPressed();
getOnBackPressedDispatcher().onBackPressed();
return true;
} else if (itemId == R.id.mark_as_read) {
DcHelper.getContext(this).marknoticedChat(DcChat.DC_CHAT_ID_ARCHIVED_LINK);
@@ -73,20 +90,6 @@ public class ConversationListArchiveActivity extends PassphraseRequiredActionBar
return false;
}
@Override
public void onBackPressed() {
if (isRelayingMessageContent(this)) {
// Go back to the ConversationListRelayingActivity
super.onBackPressed();
} else {
// Load the ConversationListActivity in case it's not existent for some reason
Intent intent = new Intent(this, ConversationListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
}
@Override
public void onCreateConversation(int chatId) {
Intent intent = new Intent(this, ConversationActivity.class);
@@ -233,7 +233,7 @@ public class ConversationListFragment extends BaseConversationListFragment
private final Object loadChatlistLock = new Object();
private boolean inLoadChatlist;
private boolean needsAnotherLoad;
private void loadChatlistAsync() {
public void loadChatlistAsync() {
synchronized (loadChatlistLock) {
needsAnotherLoad = true;
if (inLoadChatlist) {
@@ -124,7 +124,6 @@ public class ConversationListItem extends RelativeLayout
this.msgId = msgId;
int state = dcSummary.getState();
int unreadCount = thread.getUnreadCount();
if (highlightSubstring != null) {
this.fromView.setText(getHighlightedSpan(recipient.getName(), highlightSubstring));
@@ -151,7 +150,7 @@ public class ConversationListItem extends RelativeLayout
thread.getVisibility()==DcChat.DC_CHAT_VISIBILITY_PINNED? R.drawable.ic_pinned_chatlist : 0, 0
);
setStatusIcons(thread.getVisibility(), state, unreadCount, thread.isContactRequest(), thread.isMuted() || chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK);
setStatusIcons(thread, state);
setBatchState(batchMode);
setBgColor(thread);
@@ -240,14 +239,14 @@ public class ConversationListItem extends RelativeLayout
return msgId;
}
private void setStatusIcons(int visibility, int state, int unreadCount, boolean isContactRequest, boolean isMuted) {
if (visibility==DcChat.DC_CHAT_VISIBILITY_ARCHIVED)
private void setStatusIcons(ThreadRecord thread, int state) {
if (thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED)
{
archivedBadgeView.setVisibility(View.VISIBLE);
requestBadgeView.setVisibility(isContactRequest ? View.VISIBLE : View.GONE);
requestBadgeView.setVisibility(thread.isContactRequest() ? View.VISIBLE : View.GONE);
deliveryStatusIndicator.setNone();
}
else if (isContactRequest) {
else if (thread.isContactRequest()) {
requestBadgeView.setVisibility(View.VISIBLE);
archivedBadgeView.setVisibility(View.GONE);
deliveryStatusIndicator.setNone();
@@ -259,30 +258,27 @@ public class ConversationListItem extends RelativeLayout
if (state == DcMsg.DC_STATE_OUT_FAILED) {
deliveryStatusIndicator.setFailed();
} else if (state == DcMsg.DC_STATE_OUT_MDN_RCVD) {
deliveryStatusIndicator.setRead();
} else if (state == DcMsg.DC_STATE_OUT_DELIVERED) {
deliveryStatusIndicator.setSent();
} else if (state == DcMsg.DC_STATE_OUT_PREPARING) {
deliveryStatusIndicator.setPreparing();
} else if (state == DcMsg.DC_STATE_OUT_PENDING) {
deliveryStatusIndicator.setPending();
} else if (thread.isOutChannel()) {
deliveryStatusIndicator.setNone();
} else if (state == DcMsg.DC_STATE_OUT_MDN_RCVD) {
deliveryStatusIndicator.setRead();
} else if (state == DcMsg.DC_STATE_OUT_DELIVERED) {
deliveryStatusIndicator.setSent();
} else {
deliveryStatusIndicator.setNone();
}
if (state == DcMsg.DC_STATE_OUT_FAILED) {
deliveryStatusIndicator.setTint(Color.RED);
} else {
deliveryStatusIndicator.resetTint();
}
}
if(unreadCount==0 || isContactRequest) {
int unreadCount = thread.getUnreadCount();
if(unreadCount==0 || thread.isContactRequest()) {
unreadIndicator.setVisibility(View.GONE);
} else {
final int color;
if(isMuted){
if(thread.isMuted() || chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK){
color = getResources().getColor(ThemeUtil.isDarkTheme(getContext()) ? R.color.unread_count_muted_dark : R.color.unread_count_muted);
} else {
final TypedArray attrs = getContext().obtainStyledAttributes(new int[] {
@@ -15,6 +15,8 @@ import com.b44t.messenger.DcMsg;
import org.json.JSONObject;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.JsonUtils;
@@ -61,9 +63,11 @@ public class ConversationUpdateItem extends BaseConversationItem
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient conversationRecipient,
boolean pulseUpdate)
boolean pulseUpdate,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener)
{
bind(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
bindPartial(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
setGenericInfoRecord(messageRecord);
}
@@ -119,12 +123,6 @@ public class ConversationUpdateItem extends BaseConversationItem
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
else if (messageRecord.isPending()) deliveryStatusView.setPending();
else deliveryStatusView.setNone();
if (messageRecord.isFailed()) {
deliveryStatusView.setTint(Color.RED);
} else {
deliveryStatusView.setTint(textColor);
}
}
@Override
@@ -21,6 +21,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.loader.app.LoaderManager;
@@ -52,8 +53,6 @@ public class CreateProfileActivity extends BaseActionBarActivity {
private static final String TAG = CreateProfileActivity.class.getSimpleName();
public static final String FROM_WELCOME = "from_welcome";
private static final int REQUEST_CODE_AVATAR = 1;
private InputAwareLayout container;
@@ -61,7 +60,6 @@ public class CreateProfileActivity extends BaseActionBarActivity {
private EditText name;
private EditText statusView;
private boolean fromWelcome;
private boolean avatarChanged;
private boolean imageLoaded;
@@ -72,12 +70,11 @@ public class CreateProfileActivity extends BaseActionBarActivity {
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.fromWelcome = getIntent().getBooleanExtra(FROM_WELCOME, false);
setContentView(R.layout.profile_create_activity);
getSupportActionBar().setTitle(R.string.pref_profile_info_headline);
getSupportActionBar().setDisplayHomeAsUpEnabled(!this.fromWelcome);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
attachmentManager = new AttachmentManager(this, () -> {});
@@ -86,6 +83,18 @@ public class CreateProfileActivity extends BaseActionBarActivity {
initializeProfileName();
initializeProfileAvatar();
initializeStatusText();
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (container.isInputOpen()) {
container.hideCurrentInput(name);
} else {
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
}
}
});
}
@Override
@@ -100,7 +109,7 @@ public class CreateProfileActivity extends BaseActionBarActivity {
super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
onBackPressed();
getOnBackPressedDispatcher().onBackPressed();
return true;
} else if (itemId == R.id.menu_create_profile) {
updateProfile();
@@ -109,17 +118,6 @@ public class CreateProfileActivity extends BaseActionBarActivity {
return false;
}
@Override
public void onBackPressed() {
if (container.isInputOpen()) {
container.hideCurrentInput(name);
} else if (fromWelcome) {
updateProfile();
} else {
super.onBackPressed();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -187,13 +185,7 @@ public class CreateProfileActivity extends BaseActionBarActivity {
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(container);
if (fromWelcome) {
loginSuccessText.setText(R.string.set_name_and_avatar_explain);
ViewUtil.findById(this, R.id.status_text_layout).setVisibility(View.GONE);
ViewUtil.findById(this, R.id.information_label).setVisibility(View.GONE);
} else {
loginSuccessText.setVisibility(View.GONE);
}
loginSuccessText.setVisibility(View.GONE);
}
private void initializeProfileName() {
@@ -260,11 +252,6 @@ public class CreateProfileActivity extends BaseActionBarActivity {
if (result) {
attachmentManager.cleanup();
if (fromWelcome) {
Intent intent = new Intent(getApplicationContext(), ConversationListActivity.class);
intent.putExtra(ConversationListActivity.FROM_WELCOME, true);
startActivity(intent);
}
finish();
} else {
Toast.makeText(CreateProfileActivity.this, R.string.error, Toast.LENGTH_LONG).show();
@@ -6,6 +6,7 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -44,12 +45,14 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Objects;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
implements ItemClickListener
{
private static final String TAG = GroupCreateActivity.class.getSimpleName();
public static final String EDIT_GROUP_CHAT_ID = "edit_group_chat_id";
public static final String CREATE_BROADCAST = "create_broadcast";
public static final String UNENCRYPTED = "unencrypted";
@@ -63,6 +66,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private boolean unencrypted;
private boolean broadcast;
private EditText groupName;
private EditText chatDescription;
private ListView lv;
private ImageView avatar;
private Bitmap avatarBmp;
@@ -140,6 +144,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
lv = ViewUtil.findById(this, R.id.selected_contacts_list);
avatar = ViewUtil.findById(this, R.id.avatar);
groupName = ViewUtil.findById(this, R.id.group_name);
chatDescription = ViewUtil.findById(this, R.id.chat_description);
TextView chatHints = ViewUtil.findById(this, R.id.chat_hints);
// add padding to avoid content hidden behind system bars
@@ -178,6 +183,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
} else if (unencrypted) {
avatar.setVisibility(View.GONE);
groupName.setHint(R.string.subject);
findViewById(R.id.chat_description_container).setVisibility(View.GONE);
chatHints.setVisibility(View.GONE);
} else {
chatHints.setVisibility(View.GONE);
@@ -186,6 +192,14 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
if(isEdit()) {
groupName.setText(dcContext.getChat(groupChatId).getName());
lv.setVisibility(View.GONE);
Rpc rpc = DcHelper.getRpc(this);
try {
String description = rpc.getChatDescription(rpc.getSelectedAccountId(), groupChatId);
chatDescription.setText(description);
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
}
}
@@ -261,22 +275,22 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
private void createGroup(String groupName) {
if (broadcast) {
try {
groupChatId = DcHelper.getRpc(this).createBroadcast(dcContext.getAccountId(), groupName);
} catch (RpcException e) {
e.printStackTrace();
return;
Rpc rpc = DcHelper.getRpc(this);
int accId;
try {
accId = rpc.getSelectedAccountId();
if (broadcast) {
groupChatId = rpc.createBroadcast(accId, groupName);
} else if (unencrypted) {
groupChatId = rpc.createGroupChatUnencrypted(accId, groupName);
} else {
groupChatId = rpc.createGroupChat(accId, groupName, false);
}
} else if (unencrypted) {
try {
groupChatId = DcHelper.getRpc(this).createGroupChatUnencrypted(dcContext.getAccountId(), groupName);
} catch (RpcException e) {
e.printStackTrace();
return;
}
} else {
groupChatId = dcContext.createGroupChat(groupName);
rpc.setChatDescription(accId, groupChatId, getChatDescription());
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
return;
}
for (int contactId : getAdapter().getContacts()) {
@@ -307,6 +321,14 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
dcContext.setChatName(groupChatId, groupName);
Rpc rpc = DcHelper.getRpc(this);
String description = getChatDescription();
try {
rpc.setChatDescription(rpc.getSelectedAccountId(), groupChatId, description);
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
if (avatarChanged) AvatarHelper.setGroupAvatar(this, groupChatId, avatarBmp);
attachmentManager.cleanup();
@@ -331,6 +353,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
return ret;
}
private @Nullable String getChatDescription() {
return chatDescription.getText() != null ? chatDescription.getText().toString().trim() : "";
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
@@ -25,7 +25,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@@ -41,7 +40,6 @@ import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import org.thoughtcrime.securesms.components.AvatarSelector;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
@@ -56,6 +54,7 @@ import org.thoughtcrime.securesms.relay.RelayListActivity;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.TextUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.ProgressDialog;
@@ -64,6 +63,8 @@ import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
@@ -76,11 +77,11 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private static final String INSTANCES_URL = "https://chatmail.at/relays";
private static final String DEFAULT_CHATMAIL_HOST = "arcanechat.me";
public static final String FROM_WELCOME = "from_welcome";
private static final int REQUEST_CODE_AVATAR = 1;
private ImageView avatar;
private EditText name;
private TextView invitationText;
private TextView privacyPolicyBtn;
private Button signUpBtn;
@@ -88,7 +89,11 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private boolean imageLoaded;
private String providerHost;
private String providerQrData;
private String rawQrData;
private DcLot parsedQrData;
private boolean isDcLogin;
private boolean isContactInvitation;
private boolean isGroupInvitation;
private AttachmentManager attachmentManager;
private Bitmap avatarBmp;
@@ -98,6 +103,8 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private DcContext dcContext;
private ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
@@ -107,9 +114,9 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
Objects.requireNonNull(getSupportActionBar()).setTitle(R.string.onboarding_create_instant_account);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
boolean fromWelcome = getIntent().getBooleanExtra(FROM_WELCOME, false);
boolean configured = DcHelper.getContext(this).isConfigured() == 1;
if (DcHelper.getContext(this).isConfigured() == 1) {
if (configured) {
// if account is configured it means we didn't come from Welcome screen nor from QR scanner,
// instead, user clicked a dcaccount:// URI directly, so we need to just offer to add a new relay
Uri uri = getIntent().getData();
@@ -117,25 +124,11 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
Intent intent = new Intent(this, RelayListActivity.class);
intent.putExtra(RelayListActivity.EXTRA_QR_DATA, uri.toString());
startActivity(intent);
finish();
return;
}
// if URI is unexpectedly null, then fallback to new profile creation
AccountManager.getInstance().beginAccountCreation(this);
finish();
return;
}
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(!fromWelcome) {
@Override
public void handleOnBackPressed() {
AccountManager accountManager = AccountManager.getInstance();
if (accountManager.canRollbackAccountCreation(InstantOnboardingActivity.this)) {
accountManager.rollbackAccountCreation(InstantOnboardingActivity.this);
} else {
finish();
}
}
});
isDcLogin = false;
providerHost = DEFAULT_CHATMAIL_HOST;
providerQrData = DCACCOUNT + ":" + providerHost;
@@ -222,17 +215,31 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private void setProviderFromQr(String rawQr) {
DcLot qrParsed = dcContext.checkQr(rawQr);
boolean isDcLogin = qrParsed.getState() == DcContext.DC_QR_LOGIN;
if (isDcLogin || qrParsed.getState() == DcContext.DC_QR_ACCOUNT) {
this.isDcLogin = isDcLogin;
providerHost = qrParsed.getText1();
providerQrData = rawQr;
updateProvider();
} else {
new AlertDialog.Builder(this)
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
.setPositiveButton(R.string.ok, null)
.show();
switch (qrParsed.getState()) {
case DcContext.DC_QR_LOGIN:
isDcLogin = true; // Intentional fall-through
case DcContext.DC_QR_ACCOUNT:
providerHost = qrParsed.getText1();
providerQrData = rawQr;
updateProvider();
break;
case DcContext.DC_QR_ASK_VERIFYCONTACT:
isContactInvitation = true;
rawQrData = rawQr;
parsedQrData = qrParsed;
updateProvider();
break;
case DcContext.DC_QR_ASK_VERIFYGROUP:
isGroupInvitation = true;
rawQrData = rawQr;
parsedQrData = qrParsed;
updateProvider();
break;
default:
new AlertDialog.Builder(this)
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
.setPositiveButton(R.string.ok, null)
.show();
}
}
@@ -276,6 +283,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
public void onDestroy() {
super.onDestroy();
DcHelper.getEventCenter(this).removeObservers(this);
executor.shutdown();
}
private void handleIntent() {
@@ -327,6 +335,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private void initializeResources() {
this.avatar = findViewById(R.id.avatar);
this.name = findViewById(R.id.name_text);
this.invitationText = findViewById(R.id.invitation_label);
this.privacyPolicyBtn = findViewById(R.id.privacy_policy_button);
this.signUpBtn = findViewById(R.id.signup_button);
@@ -341,7 +350,10 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
signUpBtn.setOnClickListener(view -> createProfile());
findViewById(R.id.use_other_server).setOnClickListener((v) -> {
Button otherServerButton = findViewById(R.id.use_other_server);
otherServerButton.setText(
TextUtil.markAsExternal(getString(R.string.instant_onboarding_other_server)));
otherServerButton.setOnClickListener((v) -> {
IntentUtils.showInBrowser(this, INSTANCES_URL);
});
findViewById(R.id.login_button).setOnClickListener((v) -> {
@@ -365,10 +377,25 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
}
if (DEFAULT_CHATMAIL_HOST.equals(providerHost)) {
privacyPolicyBtn.setText(getString(R.string.instant_onboarding_agree_default2, providerHost));
privacyPolicyBtn.setText(TextUtil.markAsExternal(
getString(R.string.instant_onboarding_agree_default2, providerHost)));
} else {
privacyPolicyBtn.setText(getString(R.string.instant_onboarding_agree_instance, providerHost));
privacyPolicyBtn.setText(TextUtil.markAsExternal(
getString(R.string.instant_onboarding_agree_instance, providerHost)));
}
if (parsedQrData != null) {
if (isContactInvitation) {
String name = dcContext.getContact(parsedQrData.getId()).getDisplayName();
invitationText.setText(this.getString(R.string.instant_onboarding_contact_info, name));
invitationText.setVisibility(View.VISIBLE);
} else if (isGroupInvitation) {
String groupName = parsedQrData.getText1();
invitationText.setText(this.getString(R.string.instant_onboarding_group_info, groupName));
invitationText.setVisibility(View.VISIBLE);
}
}
}
}
@@ -430,11 +457,14 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
Intent intent = new Intent(getApplicationContext(), ConversationListActivity.class);
intent.putExtra(ConversationListActivity.FROM_WELCOME, true);
if (isContactInvitation || isGroupInvitation) {
intent.putExtra(ConversationListActivity.FROM_WELCOME_RAW_QR, rawQrData);
}
startActivity(intent);
finishAffinity();
}
@SuppressLint("StaticFieldLeak")
private void createProfile() {
if (TextUtils.isEmpty(this.name.getText())) {
Toast.makeText(this, R.string.please_enter_name, Toast.LENGTH_LONG).show();
@@ -442,37 +472,31 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
}
final String name = this.name.getText().toString();
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
Context context = InstantOnboardingActivity.this;
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
executor.execute(() -> {
Context context = InstantOnboardingActivity.this;
DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name);
if (avatarChanged) {
try {
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
boolean result = true;
if (avatarChanged) {
try {
AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp);
Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt());
} catch (IOException e) {
Log.w(TAG, e);
result = false;
}
return true;
}
@Override
public void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result) {
boolean finalResult = result;
runOnUiThread(() -> {
if (finalResult) {
attachmentManager.cleanup();
startQrAccountCreation(providerQrData);
} else {
Toast.makeText(InstantOnboardingActivity.this, R.string.error, Toast.LENGTH_LONG).show();
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
});
}
private void startQrAccountCreation(String qrCode)
@@ -5,6 +5,9 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.activity.OnBackPressedCallback;
import org.thoughtcrime.securesms.util.TextUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.InputStream;
@@ -44,6 +47,18 @@ public class LocalHelpActivity extends WebViewActivity
e.printStackTrace();
}
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
}
}
});
webView.loadUrl("file:///android_asset/" + helpPath.replace("LANG", helpLang) + (section!=null? section : ""));
}
@@ -51,6 +66,28 @@ public class LocalHelpActivity extends WebViewActivity
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
this.getMenuInflater().inflate(R.menu.local_help, menu);
// Append "" to external link buttons
MenuItem item = menu.findItem(R.id.learn_more);
if (item != null) {
item.setTitle(TextUtil.markAsExternal(getString(R.string.delta_chat_homepage)));
}
item = menu.findItem(R.id.privacy_policy);
if (item != null) {
item.setTitle(TextUtil.markAsExternal(getString(R.string.privacy_policy)));
}
item = menu.findItem(R.id.contribute);
if (item != null) {
item.setTitle(TextUtil.markAsExternal(getString(R.string.contribute)));
}
item = menu.findItem(R.id.report_issue);
if (item != null) {
item.setTitle(TextUtil.markAsExternal(getString(R.string.global_menu_help_report_desktop)));
}
return true;
}
@@ -77,15 +114,6 @@ public class LocalHelpActivity extends WebViewActivity
return false;
}
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
private boolean assetExists(String fileName) {
// test using AssetManager.open();
// AssetManager.list() is unreliable eg. on my Android 7 Moto G
@@ -129,7 +129,7 @@ public class LogViewFragment extends Fragment {
return logFile;
}
private static String grabLogcat(LogViewFragment fragment) {
public static String grabLogcat() {
String command = "logcat -v threadtime -d -t 10000 *:I";
try {
final Process process = Runtime.getRuntime().exec(command);
@@ -165,7 +165,7 @@ public class LogViewFragment extends Fragment {
if (fragment == null) return null;
return "**This log may contain sensitive information. If you want to post it publicly you may examine and edit it beforehand.**\n\n" +
buildDescription(fragment) + "\n" + grabLogcat(fragment);
buildDescription(fragment) + "\n" + grabLogcat();
}
@Override
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
@@ -12,6 +13,7 @@ import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import com.b44t.messenger.DcChat;
@@ -32,7 +34,6 @@ public abstract class MessageSelectorFragment
implements DcEventCenter.DcEventDelegate
{
protected ActionMode actionMode;
protected DcContext dcContext;
protected abstract void setCorrectMenuVisibility(Menu menu);
@@ -48,7 +49,7 @@ public abstract class MessageSelectorFragment
protected void handleDisplayDetails(DcMsg dcMsg) {
View view = View.inflate(getActivity(), R.layout.message_details_view, null);
TextView detailsText = view.findViewById(R.id.details_text);
detailsText.setText(dcContext.getMsgInfo(dcMsg.getId()));
detailsText.setText(DcHelper.getContext(getContext()).getMsgInfo(dcMsg.getId()));
AlertDialog d = new AlertDialog.Builder(getActivity())
.setView(view)
@@ -58,10 +59,15 @@ public abstract class MessageSelectorFragment
}
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords) {
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords));
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), null, null);
}
protected void handleDeleteMessages(int chatId, final int[] messageIds) {
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), deleteForMeListenerExtra, deleteForAllListenerExtra);
}
protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
DcContext dcContext = DcHelper.getContext(getContext());
DcChat dcChat = dcContext.getChat(chatId);
boolean canDeleteForAll = true;
if (dcChat.isEncrypted() && dcChat.canSend() && !dcChat.isSelfTalk()) {
@@ -79,20 +85,24 @@ public abstract class MessageSelectorFragment
String text = getActivity().getResources().getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length);
int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me;
DialogInterface.OnClickListener deleteForMeListener = (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
if (actionMode != null) actionMode.finish();
if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds);
};
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
.setMessage(text)
.setCancelable(true)
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton(positiveBtnLabel, (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
if (actionMode != null) actionMode.finish();
});
.setPositiveButton(positiveBtnLabel, deleteForMeListener);
if(canDeleteForAll) {
builder.setNegativeButton(R.string.delete_for_everyone, (d, which) -> {
DialogInterface.OnClickListener deleteForAllListener = (d, which) -> {
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
if (actionMode != null) actionMode.finish();
});
if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds);
};
builder.setNegativeButton(R.string.delete_for_everyone, deleteForAllListener);
AlertDialog dialog = builder.show();
Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE);
Util.redPositiveButton(dialog);
@@ -134,7 +144,7 @@ public abstract class MessageSelectorFragment
protected void handleShowInChat(final DcMsg dcMsg) {
Intent intent = new Intent(getContext(), ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcMsg.getChatId());
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcMsg, dcContext));
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcMsg, DcHelper.getContext(getContext())));
startActivity(intent);
}
@@ -144,6 +154,7 @@ public abstract class MessageSelectorFragment
protected void handleResendMessage(final Set<DcMsg> dcMsgsSet) {
int[] ids = DcMsg.msgSetToIds(dcMsgsSet);
DcContext dcContext = DcHelper.getContext(getContext());
Util.runOnAnyBackgroundThread(() -> {
boolean success = dcContext.resendMsgs(ids);
Util.runOnMain(() -> {
@@ -22,10 +22,7 @@ import static org.thoughtcrime.securesms.util.ShareUtil.acquireRelayMessageConte
import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
@@ -37,7 +34,6 @@ import com.google.zxing.integration.android.IntentResult;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.qr.QrActivity;
import org.thoughtcrime.securesms.qr.QrCodeHandler;
import org.thoughtcrime.securesms.util.MailtoUtil;
import chat.delta.rpc.types.SecurejoinSource;
import chat.delta.rpc.types.SecurejoinUiPath;
@@ -57,53 +53,6 @@ public class NewConversationActivity extends ContactSelectionActivity {
super.onCreate(bundle, ready);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
handleIntent();
}
private void handleIntent() {
Intent intent = getIntent();
String action = intent.getAction();
if(Intent.ACTION_VIEW.equals(action) || Intent.ACTION_SENDTO.equals(action)) {
try {
Uri uri = intent.getData();
if(uri != null) {
String scheme = uri.getScheme();
if(MailtoUtil.isMailto(uri)) {
String textToShare = MailtoUtil.getText(uri);
String[] recipientsArray = MailtoUtil.getRecipients(uri);
if (recipientsArray.length >= 1) {
if (!textToShare.isEmpty()) {
getIntent().putExtra(TEXT_EXTRA, textToShare);
}
final String addr = recipientsArray[0];
final DcContext dcContext = DcHelper.getContext(this);
int contactId = dcContext.lookupContactIdByAddr(addr);
if (contactId == 0 && dcContext.mayBeValidAddr(addr)) {
contactId = dcContext.createContact(null, recipientsArray[0]);
}
if (contactId == 0) {
Toast.makeText(this, R.string.bad_email_address, Toast.LENGTH_LONG).show();
} else {
onContactSelected(contactId);
}
} else {
Intent shareIntent = new Intent(this, ShareActivity.class);
shareIntent.putExtra(Intent.EXTRA_TEXT, textToShare);
startActivity(shareIntent);
finish();
}
} else if(scheme != null && scheme.startsWith("http")) {
Intent shareIntent = new Intent(this, ShareActivity.class);
shareIntent.putExtra(Intent.EXTRA_TEXT, uri.toString());
startActivity(shareIntent);
finish();
}
}
}
catch(Exception e) {
Log.e(TAG, "start activity from external 'mailto:' link failed", e);
}
}
}
@Override
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -30,16 +31,21 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class ProfileAdapter extends RecyclerView.Adapter
{
private static final String TAG = ProfileAdapter.class.getSimpleName();
public static final int ITEM_AVATAR = 10;
public static final int ITEM_DIVIDER = 20;
public static final int ITEM_SIGNATURE = 25;
public static final int ITEM_ALL_MEDIA_BUTTON = 30;
public static final int ITEM_SEND_MESSAGE_BUTTON = 35;
public static final int ITEM_LAST_SEEN = 40;
public static final int ITEM_BLOCKED = 43;
public static final int ITEM_INTRODUCED_BY = 45;
public static final int ITEM_ADDRESS = 50;
public static final int ITEM_HEADER = 53;
public static final int ITEM_MEMBERS = 55;
public static final int ITEM_SHARED_CHATS = 60;
@@ -140,7 +146,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
} else if (viewType == ITEM_ALL_MEDIA_BUTTON || viewType == ITEM_SEND_MESSAGE_BUTTON) {
final ProfileTextItem item = (ProfileTextItem)layoutInflater.inflate(R.layout.profile_text_item_button, parent, false);
return new ViewHolder(item);
} else if (viewType == ITEM_LAST_SEEN || viewType == ITEM_INTRODUCED_BY || viewType == ITEM_ADDRESS) {
} else if (viewType == ITEM_LAST_SEEN || viewType == ITEM_INTRODUCED_BY || viewType == ITEM_BLOCKED) {
final ProfileTextItem item = (ProfileTextItem)layoutInflater.inflate(R.layout.profile_text_item_small, parent, false);
return new ViewHolder(item);
} else {
@@ -195,7 +201,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
}
else if(holder.itemView instanceof ProfileStatusItem) {
ProfileStatusItem item = (ProfileStatusItem) holder.itemView;
item.setOnLongClickListener(view -> {clickListener.onStatusLongClicked(); return true;});
item.setOnLongClickListener(view -> {clickListener.onStatusLongClicked(dcContact == null); return true;});
item.set(data.label);
}
else if(holder.itemView instanceof ProfileAvatarItem) {
@@ -206,14 +212,11 @@ public class ProfileAdapter extends RecyclerView.Adapter
else if(holder.itemView instanceof ProfileTextItem) {
ProfileTextItem item = (ProfileTextItem) holder.itemView;
item.setOnClickListener(view -> clickListener.onSettingsClicked(data.viewType));
boolean tintIcon = data.viewType != ITEM_INTRODUCED_BY;
boolean tintIcon = data.viewType != ITEM_INTRODUCED_BY && data.viewType != ITEM_BLOCKED;
item.set(data.label, data.icon, tintIcon);
if (data.viewType == ITEM_LAST_SEEN || data.viewType == ITEM_ADDRESS) {
if (data.viewType == ITEM_BLOCKED) {
int padding = (int)((float)context.getResources().getDimensionPixelSize(R.dimen.contact_list_normal_padding) * 1.2);
item.setPadding(item.getPaddingLeft(), item.getPaddingTop(), item.getPaddingRight(), padding);
if (data.viewType == ITEM_ADDRESS) {
fragment.registerForContextMenu(item);
}
} else if (data.viewType == ITEM_INTRODUCED_BY) {
int padding = context.getResources().getDimensionPixelSize(R.dimen.contact_list_normal_padding);
item.setPadding(item.getPaddingLeft(), padding, item.getPaddingRight(), item.getPaddingBottom());
@@ -233,7 +236,7 @@ public class ProfileAdapter extends RecyclerView.Adapter
public interface ItemClickListener {
void onSettingsClicked(int settingsId);
void onStatusLongClicked();
void onStatusLongClicked(boolean isMultiUser);
void onSharedChatClicked(int chatId);
void onMemberClicked(int contactId);
void onMemberLongClicked(int contactId);
@@ -281,8 +284,21 @@ public class ProfileAdapter extends RecyclerView.Adapter
itemData.add(new ItemData(ITEM_AVATAR, null, 0));
if (isSelfTalk || dcContact != null && !dcContact.getStatus().isEmpty()) {
itemDataStatusText = isSelfTalk ? context.getString(R.string.saved_messages_explain) : dcContact.getStatus();
if (isSelfTalk) {
itemDataStatusText = context.getString(R.string.saved_messages_explain);
} else if (dcContact != null) {
itemDataStatusText = dcContact.getStatus();
} else if (dcChat != null && dcChat.isEncrypted()) {
// Load group or channel description
try {
Rpc rpc = DcHelper.getRpc(context);
itemDataStatusText = rpc.getChatDescription(rpc.getSelectedAccountId(), dcChat.getId());
} catch (RpcException e) {
Log.e(TAG, "RPC error", e);
}
}
if (!itemDataStatusText.isEmpty()) {
itemData.add(new ItemData(ITEM_SIGNATURE, itemDataStatusText, 0));
} else {
itemData.add(new ItemData(ITEM_DIVIDER, null, 0));
@@ -308,6 +324,10 @@ public class ProfileAdapter extends RecyclerView.Adapter
}
*/
if (dcContact != null && !isDeviceTalk && !isSelfTalk && dcContact.isBlocked()) {
itemData.add(new ItemData(ITEM_BLOCKED, context.getString(R.string.contact_blocked), R.drawable.contact_blocked_24));
}
if (memberList!=null && !isInBroadcast && !isMailingList) {
itemData.add(new ItemData(ITEM_DIVIDER, null, 0));
if (dcChat != null) {
@@ -345,10 +365,6 @@ public class ProfileAdapter extends RecyclerView.Adapter
String introducedBy = context.getString(R.string.verified_by_unknown);
itemData.add(new ItemData(ITEM_INTRODUCED_BY, introducedBy, R.drawable.ic_verified));
}
if (dcContact != null) {
itemData.add(new ItemData(ITEM_ADDRESS, dcContact.getAddr(), 0));
}
}
notifyDataSetChanged();
@@ -140,10 +140,10 @@ public class ProfileFragment extends Fragment
}
@Override
public void onStatusLongClicked() {
public void onStatusLongClicked(boolean isMultiUser) {
Context context = requireContext();
new AlertDialog.Builder(context)
.setTitle(R.string.pref_default_status_label)
.setTitle(isMultiUser? R.string.chat_description : R.string.pref_default_status_label)
.setItems(new CharSequence[]{
context.getString(R.string.menu_copy_to_clipboard)
},
@@ -310,11 +310,27 @@ public class ProfileFragment extends Fragment
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==REQUEST_CODE_PICK_CONTACT && resultCode==Activity.RESULT_OK && data!=null) {
List<Integer> selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA);
if(selected == null) return;
List<Integer> deselected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA);
Util.runOnAnyBackgroundThread(() -> {
for (Integer contactId : selected) {
if (contactId!=null) {
dcContext.addContactToChat(chatId, contactId);
if (deselected != null) {
// Remove members that were deselected
int[] members = dcContext.getChatContacts(chatId);
for (int contactId : deselected) {
for (int memberId : members) {
if (memberId == contactId) {
dcContext.removeContactFromChat(chatId, memberId);
break;
}
}
}
}
if (selected != null) {
// Add new members
for (Integer contactId : selected) {
if (contactId != null) {
dcContext.addContactToChat(chatId, contactId);
}
}
}
});
@@ -104,26 +104,25 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
resolvedExtras = new ArrayList<>();
List<Uri> streamExtras = new ArrayList<>();
if (MailtoUtil.isMailto(getIntent().getData())) {
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
if (extraEmail == null || extraEmail.length == 0) {
getIntent().putExtra(Intent.EXTRA_EMAIL, MailtoUtil.getRecipients(getIntent().getData()));
}
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (text == null || text.isEmpty()) {
getIntent().putExtra(Intent.EXTRA_TEXT, MailtoUtil.getText(getIntent().getData()));
}
}
if (Intent.ACTION_SEND.equals(getIntent().getAction()) &&
getIntent().getParcelableExtra(Intent.EXTRA_STREAM) != null) {
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
streamExtras.add(uri);
} else if (getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM) != null) {
streamExtras = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
} else {
Uri uri = getIntent().getData();
if (MailtoUtil.isMailto(uri)) {
String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL);
if (extraEmail == null || extraEmail.length == 0) {
getIntent().putExtra(Intent.EXTRA_EMAIL, MailtoUtil.getRecipients(uri));
}
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (text == null || text.isEmpty()) {
getIntent().putExtra(Intent.EXTRA_TEXT, MailtoUtil.getText(uri));
}
} else if (uri != null) {
streamExtras.add(uri);
}
} else if (getIntent().getData() != null) {
streamExtras.add(getIntent().getData());
}
if (needsFilePermission(streamExtras)) {
@@ -265,6 +264,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity implement
}
chatId = dcContext.createChatByContactId(contactId);
accId = dcContext.getAccountId();
}
Intent composeIntent;
if (accId != -1 && chatId != -1) {
@@ -77,7 +77,7 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
findViewById(R.id.status_bar_background).setBackgroundResource(R.drawable.search_toolbar_shadow);
} else {
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(findViewById(R.id.content_container));
ViewUtil.applyWindowInsets(findViewById(R.id.content_container), true, true, true, true, true, false);
}
webView.setWebViewClient(new WebViewClient() {
@@ -15,6 +15,7 @@ import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@@ -36,6 +37,7 @@ import org.thoughtcrime.securesms.qr.QrCodeHandler;
import org.thoughtcrime.securesms.qr.RegistrationQrActivity;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.StreamUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -67,16 +69,48 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(findViewById(R.id.content_container));
findViewById(R.id.signup_button).setOnClickListener((v) -> startInstantOnboardingActivity());
findViewById(R.id.add_as_second_device_button).setOnClickListener((v) -> startAddAsSecondDeviceActivity());
findViewById(R.id.signup_button).setOnClickListener((v) -> startActivity(new Intent(this, InstantOnboardingActivity.class)));
findViewById(R.id.add_as_second_device_button).setOnClickListener((v) -> showSignInDialogWithPermission());
findViewById(R.id.backup_button).setOnClickListener((v) -> startImportBackup());
registerForEvents();
initializeActionBar();
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
AccountManager accountManager = AccountManager.getInstance();
if (accountManager.canRollbackAccountCreation(WelcomeActivity.this)) {
accountManager.rollbackAccountCreation(WelcomeActivity.this);
} else {
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
}
}
});
DcHelper.maybeShowMigrationError(this);
}
private void showSignInDialogWithPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& !Prefs.getBooleanPreference(this, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, false)) {
Prefs.setBooleanPreference(this, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, true);
Permissions.with(this)
.request(Manifest.permission.POST_NOTIFICATIONS)
.ifNecessary()
.onAllGranted(() -> {
startAddAsSecondDeviceActivity();
})
.onAnyDenied(() -> {
startAddAsSecondDeviceActivity();
})
.execute();
} else {
startAddAsSecondDeviceActivity();
}
}
protected void initializeActionBar() {
ActionBar supportActionBar = getSupportActionBar();
if (supportActionBar == null) throw new AssertionError();
@@ -97,7 +131,7 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
getOnBackPressedDispatcher().onBackPressed();
return true;
}
@@ -131,12 +165,6 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void startInstantOnboardingActivity() {
Intent intent = new Intent(this, InstantOnboardingActivity.class);
intent.putExtra(InstantOnboardingActivity.FROM_WELCOME, true);
startActivity(intent);
}
private void startAddAsSecondDeviceActivity() {
new IntentIntegrator(this).setCaptureActivity(RegistrationQrActivity.class)
.addExtra(RegistrationQrActivity.ADD_AS_SECOND_DEVICE_EXTRA, true)
@@ -352,14 +380,4 @@ public class WelcomeActivity extends BaseActionBarActivity implements DcEventCen
e.printStackTrace();
}
}
@Override
public void onBackPressed() {
AccountManager accountManager = AccountManager.getInstance();
if (accountManager.canRollbackAccountCreation(this)) {
accountManager.rollbackAccountCreation(this);
} else {
super.onBackPressed();
}
}
}
@@ -47,24 +47,31 @@ import chat.delta.rpc.RpcException;
public class AccountSelectionListFragment extends DialogFragment implements DcEventCenter.DcEventDelegate
{
private static final String TAG = AccountSelectionListFragment.class.getSimpleName();
private final ConversationListActivity activity;
private static final String ARG_SELECT_ONLY = "select_only";
private RecyclerView recyclerView;
private AccountSelectionListAdapter adapter;
private boolean selectOnly;
public AccountSelectionListFragment(ConversationListActivity activity) {
super();
this.activity = activity;
public static AccountSelectionListFragment newInstance(boolean selectOnly) {
AccountSelectionListFragment fragment = new AccountSelectionListFragment();
Bundle args = new Bundle();
args.putBoolean(ARG_SELECT_ONLY, selectOnly);
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
selectOnly = getArguments() != null && getArguments().getBoolean(ARG_SELECT_ONLY, false);
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
.setTitle(R.string.switch_account)
.setNeutralButton(R.string.connectivity, ((dialog, which) -> {
startActivity(new Intent(getActivity(), ConnectivityActivity.class));
}))
.setNegativeButton(R.string.cancel, null);
if (!selectOnly) {
builder.setNeutralButton(R.string.connectivity, ((dialog, which) -> {
startActivity(new Intent(getActivity(), ConnectivityActivity.class));
}));
}
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.account_selection_list_fragment, null);
@@ -99,18 +106,20 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
DcAccounts accounts = DcHelper.getAccounts(getActivity());
int[] accountIds = accounts.getAll();
int[] ids = new int[accountIds.length + 1];
int[] ids = new int[(selectOnly? 0 : 1) + accountIds.length];
int j = 0;
for (int accountId : accountIds) {
ids[j++] = accountId;
}
ids[j] = DC_CONTACT_ID_ADD_ACCOUNT;
if (!selectOnly) ids[j] = DC_CONTACT_ID_ADD_ACCOUNT;
adapter.changeData(ids, accounts.getSelectedAccount().getAccountId());
}
@Override
public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
if (selectOnly) return;
requireActivity().getMenuInflater().inflate(R.menu.account_item_context, menu);
AccountSelectionListItem listItem = (AccountSelectionListItem) v;
@@ -173,6 +182,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
}
private void onSetTag(int accountId) {
ConversationListActivity activity = (ConversationListActivity)requireActivity();
AccountSelectionListFragment.this.dismiss();
DcContext dcContext = DcHelper.getAccounts(activity).getAccount(accountId);
@@ -188,14 +198,15 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
.setPositiveButton(android.R.string.ok, (d, b) -> {
String newTag = inputField.getText().toString().trim();
dcContext.setConfig(CONFIG_PRIVATE_TAG, newTag);
AccountManager.getInstance().showSwitchAccountMenu(activity);
AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly);
})
.setNegativeButton(R.string.cancel, (d, b) -> AccountManager.getInstance().showSwitchAccountMenu(activity))
.setNegativeButton(R.string.cancel, (d, b) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly))
.show();
}
private void onDeleteProfile(int accountId) {
AccountSelectionListFragment.this.dismiss();
ConversationListActivity activity = (ConversationListActivity)requireActivity();
DcAccounts accounts = DcHelper.getAccounts(activity);
Rpc rpc = DcHelper.getRpc(activity);
@@ -230,7 +241,7 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.delete_account)
.setView(dialogView)
.setNegativeButton(R.string.cancel, (d, which) -> AccountManager.getInstance().showSwitchAccountMenu(activity))
.setNegativeButton(R.string.cancel, (d, which) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly))
.setPositiveButton(R.string.delete, (d2, w2) -> activity.onDeleteProfile(accountId))
.show();
Util.redPositiveButton(dialog);
@@ -247,13 +258,14 @@ public class AccountSelectionListFragment extends DialogFragment implements DcEv
@Override
public void onItemClick(AccountSelectionListItem contact) {
Activity activity = requireActivity();
AccountSelectionListFragment.this.dismiss();
ConversationListActivity activity = (ConversationListActivity)requireActivity();
int accountId = contact.getAccountId();
if (accountId == DC_CONTACT_ID_ADD_ACCOUNT) {
AccountManager.getInstance().switchAccountAndStartActivity(activity, 0);
} else if (accountId != DcHelper.getAccounts(activity).getSelectedAccount().getAccountId()) {
AccountManager.getInstance().switchAccountAndStartActivity(activity, accountId);
AccountManager.getInstance().switchAccount(activity, accountId);
activity.onProfileSwitched(accountId);
}
}
}
@@ -1,356 +0,0 @@
package org.thoughtcrime.securesms.audio;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.guava.Optional;
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
import java.io.IOException;
import java.lang.ref.WeakReference;
public class AudioSlidePlayer {
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
private final @NonNull Context context;
private final @NonNull AudioSlide slide;
private final @NonNull Handler progressEventHandler;
private @NonNull WeakReference<Listener> listener;
private @Nullable SimpleExoPlayer mediaPlayer;
private @Nullable SimpleExoPlayer durationCalculator;
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
playing.get().setListener(listener);
return playing.get();
} else {
return new AudioSlidePlayer(context, slide, listener);
}
}
private AudioSlidePlayer(@NonNull Context context,
@NonNull AudioSlide slide,
@NonNull Listener listener)
{
this.context = context;
this.slide = slide;
this.listener = new WeakReference<>(listener);
this.progressEventHandler = new ProgressEventHandler(this);
}
public void requestDuration() {
try {
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
durationCalculator = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
.setTrackSelector(new DefaultTrackSelector(context))
.setLoadControl(loadControl)
.build();
durationCalculator.setPlayWhenReady(false);
durationCalculator.addListener(new Player.Listener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == Player.STATE_READY) {
Util.runOnMain(() -> {
synchronized (AudioSlidePlayer.this) {
if (durationCalculator == null) return;
Log.d(TAG, "request duration " + durationCalculator.getDuration());
getListener().onReceivedDuration(Long.valueOf(durationCalculator.getDuration()).intValue());
durationCalculator.release();
durationCalculator.removeListener(this);
durationCalculator = null;
}
});
}
}
});
durationCalculator.prepare(createMediaSource(slide.getUri()));
} catch (Exception e) {
Log.w(TAG, e);
getListener().onReceivedDuration(0);
}
}
public void play(final double progress) throws IOException {
play(progress, false);
}
private void play(final double progress, boolean earpiece) throws IOException {
if (this.mediaPlayer != null) {
return;
}
if (slide.getUri() == null) {
throw new IOException("Slide has no URI!");
}
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
this.mediaPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
.setTrackSelector(new DefaultTrackSelector(context))
.setLoadControl(loadControl)
.build();
mediaPlayer.prepare(createMediaSource(slide.getUri()));
mediaPlayer.setPlayWhenReady(true);
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
.setContentType(earpiece ? C.AUDIO_CONTENT_TYPE_SPEECH : C.AUDIO_CONTENT_TYPE_MUSIC)
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
.build(), false);
mediaPlayer.addListener(new Player.Listener() {
boolean started = false;
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
switch (playbackState) {
case Player.STATE_READY:
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
synchronized (AudioSlidePlayer.this) {
if (mediaPlayer == null) return;
Log.d(TAG, "DURATION: " + mediaPlayer.getDuration());
if (started) {
Log.d(TAG, "Already started. Ignoring.");
return;
}
started = true;
if (progress > 0) {
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
}
setPlaying(AudioSlidePlayer.this);
}
keepScreenOn(true);
notifyOnStart();
progressEventHandler.sendEmptyMessage(0);
break;
case Player.STATE_ENDED:
Log.i(TAG, "onComplete");
synchronized (AudioSlidePlayer.this) {
getListener().onReceivedDuration(Long.valueOf(mediaPlayer.getDuration()).intValue());
mediaPlayer.release();
mediaPlayer = null;
}
keepScreenOn(false);
notifyOnStop();
progressEventHandler.removeMessages(0);
}
}
@Override
public void onPlayerError(PlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error);
synchronized (AudioSlidePlayer.this) {
mediaPlayer.release();
mediaPlayer = null;
}
notifyOnStop();
progressEventHandler.removeMessages(0);
// Failed to play media file, maybe another app can handle it
int msgId = getAudioSlide().getDcMsgId();
DcHelper.openForViewOrShare(context, msgId, Intent.ACTION_VIEW);
}
});
}
private MediaSource createMediaSource(@NonNull Uri uri) {
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(defaultDataSourceFactory);
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
return new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory)
.createMediaSource(MediaItem.fromUri(uri));
}
public synchronized void stop() {
Log.i(TAG, "Stop called!");
keepScreenOn(false);
removePlaying(this);
if (this.mediaPlayer != null) {
this.mediaPlayer.stop();
this.mediaPlayer.release();
}
this.mediaPlayer = null;
}
public static void stopAll() {
if (playing.isPresent()) {
synchronized (AudioSlidePlayer.class) {
if (playing.isPresent()) {
playing.get().stop();
}
}
}
}
public void setListener(@NonNull Listener listener) {
this.listener = new WeakReference<>(listener);
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
notifyOnStart();
}
}
public @NonNull AudioSlide getAudioSlide() {
return slide;
}
private Pair<Double, Integer> getProgress() {
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
return new Pair<>(0D, 0);
} else {
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
(int) mediaPlayer.getCurrentPosition());
}
}
private void notifyOnStart() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStart();
}
});
}
private void notifyOnStop() {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onStop();
}
});
}
private void notifyOnProgress(final double progress, final long millis) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
getListener().onProgress(slide, progress, millis);
}
});
}
private @NonNull Listener getListener() {
Listener listener = this.listener.get();
if (listener != null) return listener;
else return new Listener() {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {}
};
}
public void keepScreenOn(boolean keepOn) {
if (context instanceof Activity) {
if (keepOn) {
((Activity) context).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
((Activity) context).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
}
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() != player) {
playing.get().notifyOnStop();
playing.get().stop();
}
playing = Optional.of(player);
}
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
if (playing.isPresent() && playing.get() == player) {
playing = Optional.absent();
}
}
public interface Listener {
void onStart();
void onStop();
void onProgress(AudioSlide slide, double progress, long millis);
void onReceivedDuration(int millis);
}
private static class ProgressEventHandler extends Handler {
private final WeakReference<AudioSlidePlayer> playerReference;
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
this.playerReference = new WeakReference<>(player);
}
@Override
public void handleMessage(@NonNull Message msg) {
AudioSlidePlayer player = playerReference.get();
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
return;
}
Pair<Double, Integer> progress = player.getProgress();
player.notifyOnProgress(progress.first, progress.second);
sendEmptyMessageDelayed(0, 50);
}
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
}
}
}
@@ -43,12 +43,14 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
public static final String EXTRA_CHAT_ID = "chat_id";
public static final String EXTRA_CALL_ID = "call_id";
public static final String EXTRA_HASH = "hash";
public static final String EXTRA_HAS_VIDEO = "has_video";
private DcContext dcContext;
private Rpc rpc;
private int accId;
private int chatId;
private int callId;
private boolean hasVideo;
private boolean ended = false;
@SuppressLint("SetJavaScriptEnabled")
@@ -58,7 +60,9 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
Bundle bundle = getIntent().getExtras();
assert bundle != null;
hasVideo = bundle.getBoolean(EXTRA_HAS_VIDEO, true);
String hash = bundle.getString(EXTRA_HASH, "");
String query = hasVideo? "" : "?noOutgoingVideoInitially";
accId = bundle.getInt(EXTRA_ACCOUNT_ID, -1);
chatId = bundle.getInt(EXTRA_CHAT_ID, 0);
callId = bundle.getInt(EXTRA_CALL_ID, 0);
@@ -94,7 +98,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> {
String url = "file:///android_asset/calls/index.html";
webView.loadUrl(url + hash);
webView.loadUrl(url + query + hash);
}).onAnyDenied(this::finish)
.execute();
}
@@ -168,7 +172,7 @@ public class CallActivity extends WebViewActivity implements DcEventCenter.DcEve
@JavascriptInterface
public void startCall(String payload) {
try {
callId = rpc.placeOutgoingCall(accId, chatId, payload);
callId = rpc.placeOutgoingCall(accId, chatId, payload, hasVideo);
} catch (RpcException e) {
Log.e(TAG, "Error", e);
}
@@ -18,28 +18,29 @@ import java.nio.charset.StandardCharsets;
public class CallUtil {
private static final String TAG = CallUtil.class.getSimpleName();
public static void startCall(Activity activity, int chatId) {
public static void startCall(Activity activity, int chatId, boolean hasVideo) {
Permissions.with(activity)
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_camera_denied))
.onAllGranted(() -> {
int accId = DcHelper.getContext(activity).getAccountId();
startCall(activity, accId, chatId);
startCall(activity, accId, chatId, hasVideo);
})
.execute();
}
public static void startCall(Context context, int accId, int chatId) {
public static void startCall(Context context, int accId, int chatId, boolean hasVideo) {
Intent intent = new Intent(context, CallActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.putExtra(CallActivity.EXTRA_HASH, "#startCall");
context.startActivity(intent);
}
public static void openCall(Context context, int accId, int chatId, int callId, String payload) {
public static void openCall(Context context, int accId, int chatId, int callId, String payload, boolean hasVideo) {
String base64 = Base64.encodeToString(payload.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
String hash = "";
try {
@@ -53,6 +54,7 @@ public class CallUtil {
intent.putExtra(CallActivity.EXTRA_ACCOUNT_ID, accId);
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatId);
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.putExtra(CallActivity.EXTRA_HASH, hash);
context.startActivity(intent);
}
@@ -1,301 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.DateUtils;
import java.io.IOException;
public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
private static final String TAG = AudioView.class.getSimpleName();
private final @NonNull AnimatingToggle controlToggle;
private final @NonNull ImageView playButton;
private final @NonNull ImageView pauseButton;
private final @NonNull SeekBar seekBar;
private final @NonNull TextView timestamp;
private final @NonNull TextView title;
private final @NonNull View mask;
private @Nullable AudioSlidePlayer audioSlidePlayer;
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
private int backwardsCounter;
public AudioView(Context context) {
this(context, null);
}
public AudioView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.audio_view, this);
this.controlToggle = (AnimatingToggle) findViewById(R.id.control_toggle);
this.playButton = (ImageView) findViewById(R.id.play);
this.pauseButton = (ImageView) findViewById(R.id.pause);
this.seekBar = (SeekBar) findViewById(R.id.seek);
this.timestamp = (TextView) findViewById(R.id.timestamp);
this.title = (TextView) findViewById(R.id.title);
this.mask = findViewById(R.id.interception_mask);
this.timestamp.setText("00:00");
this.playButton.setOnClickListener(new PlayClickedListener());
this.pauseButton.setOnClickListener(new PauseClickedListener());
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
this.playButton.setImageDrawable(context.getDrawable(R.drawable.play_icon));
this.pauseButton.setImageDrawable(context.getDrawable(R.drawable.pause_icon));
this.playButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
this.pauseButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
setTint(getContext().getResources().getColor(R.color.audio_icon));
}
public void setAudio(final @NonNull AudioSlide audio, int duration)
{
controlToggle.displayQuick(playButton);
seekBar.setEnabled(true);
seekBar.setProgress(0);
audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
timestamp.setText(DateUtils.getFormatedDuration(duration));
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
title.setVisibility(View.GONE);
}
else {
title.setText(audio.getFileName().get());
title.setVisibility(View.VISIBLE);
}
}
@Override
public void setOnClickListener(OnClickListener listener) {
super.setOnClickListener(listener);
this.mask.setOnClickListener(listener);
}
@Override
public void setOnLongClickListener(OnLongClickListener listener) {
super.setOnLongClickListener(listener);
this.mask.setOnLongClickListener(listener);
this.playButton.setOnLongClickListener(listener);
this.pauseButton.setOnLongClickListener(listener);
}
public void togglePlay() {
if (this.playButton.getVisibility() == View.VISIBLE) {
playButton.performClick();
} else {
pauseButton.performClick();
}
}
public String getDescription() {
String desc;
if (this.title.getVisibility() == View.VISIBLE) {
desc = getContext().getString(R.string.audio);
} else {
desc = getContext().getString(R.string.voice_message);
}
desc += "\n" + this.timestamp.getText();
if (title.getVisibility() == View.VISIBLE) {
desc += "\n" + this.title.getText();
}
return desc;
}
public void setDuration(int duration) {
if (getProgress()==0)
this.timestamp.setText(DateUtils.getFormatedDuration(duration));
}
public void cleanup() {
if (this.audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
this.audioSlidePlayer.stop();
}
}
@Override
public void onReceivedDuration(int millis) {
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
}
@Override
public void onStart() {
if (this.pauseButton.getVisibility() != View.VISIBLE) {
togglePlayToPause();
}
}
@Override
public void onStop() {
if (this.playButton.getVisibility() != View.VISIBLE) {
togglePauseToPlay();
}
if (seekBar.getProgress() + 5 >= seekBar.getMax()) {
backwardsCounter = 4;
onProgress(audioSlidePlayer.getAudioSlide(), 0.0, -1);
}
}
public void disablePlayer(boolean disable) {
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {
if (!audioSlidePlayer.getAudioSlide().equals(slide)) {
return;
}
int seekProgress = (int) Math.floor(progress * this.seekBar.getMax());
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
backwardsCounter = 0;
this.seekBar.setProgress(seekProgress);
if (millis != -1) {
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
}
} else {
backwardsCounter++;
}
}
public void setTint(int foregroundTint) {
this.playButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
this.pauseButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
}
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
seekBar.getGlobalVisibleRect(rect);
}
private double getProgress() {
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
return 0;
} else {
return (double)this.seekBar.getProgress() / (double)this.seekBar.getMax();
}
}
private void togglePlayToPause() {
controlToggle.displayQuick(pauseButton);
AnimatedVectorDrawable playToPauseDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.play_to_pause_animation);
pauseButton.setImageDrawable(playToPauseDrawable);
playToPauseDrawable.start();
}
private void togglePauseToPlay() {
controlToggle.displayQuick(playButton);
AnimatedVectorDrawable pauseToPlayDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.pause_to_play_animation);
playButton.setImageDrawable(pauseToPlayDrawable);
pauseToPlayDrawable.start();
}
private class PlayClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
try {
Log.w(TAG, "playbutton onClick");
if (audioSlidePlayer != null) {
if (Build.VERSION.SDK_INT >= 26) {
if (audioFocusChangeListener == null) {
audioFocusChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
pauseButton.performClick();
}
};
}
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(false)
.setWillPauseWhenDucked(false)
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.build();
AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
audioManager.requestAudioFocus(focusRequest);
}
togglePlayToPause();
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
private class PauseClickedListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Log.w(TAG, "pausebutton onClick");
if (audioSlidePlayer != null) {
togglePauseToPlay();
audioSlidePlayer.stop();
}
}
}
private class SeekBarModifiedListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
@Override
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
audioSlidePlayer.stop();
}
}
@Override
public synchronized void onStopTrackingTouch(SeekBar seekBar) {
try {
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
audioSlidePlayer.play(getProgress());
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}
@@ -67,10 +67,14 @@ public class CallItemView extends FrameLayout {
title.setText(R.string.canceled_call);
} else if (callInfo.state instanceof CallState.Declined) {
title.setText(R.string.declined_call);
} else if (callInfo.hasVideo) {
title.setText(isOutgoing? R.string.outgoing_video_call : R.string.incoming_video_call);
} else {
title.setText(isOutgoing? R.string.outgoing_call : R.string.incoming_call);
title.setText(isOutgoing? R.string.outgoing_audio_call : R.string.incoming_audio_call);
}
icon.setImageResource(callInfo.hasVideo? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24);
int[] attrs;
if (isOutgoing) {
attrs = new int[]{
@@ -15,39 +15,51 @@ import androidx.annotation.Nullable;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.DateUtils;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class ConversationItemFooter extends LinearLayout {
private TextView dateView;
private TextView editedView;
private TextView viewsLabel;
private ImageView viewsIcon;
private ImageView bookmarkIndicatorView;
private ImageView emailIndicatorView;
private ImageView locationIndicatorView;
private DeliveryStatusView deliveryStatusView;
private Integer textColor = null;
private int callDuration = 0;
private Context context;
private Rpc rpc;
public ConversationItemFooter(Context context) {
super(context);
init(null);
init(context, null);
}
public ConversationItemFooter(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
init(context, attrs);
}
public ConversationItemFooter(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
init(context, attrs);
}
private void init(@Nullable AttributeSet attrs) {
private void init(Context context, @Nullable AttributeSet attrs) {
this.context = context;
this.rpc = DcHelper.getRpc(context);
inflate(getContext(), R.layout.conversation_item_footer, this);
dateView = findViewById(R.id.footer_date);
editedView = findViewById(R.id.footer_edited);
viewsLabel = findViewById(R.id.footer_views);
viewsIcon = findViewById(R.id.footer_views_icon);
bookmarkIndicatorView = findViewById(R.id.footer_bookmark_indicator);
emailIndicatorView = findViewById(R.id.footer_email_indicator);
locationIndicatorView = findViewById(R.id.footer_location_indicator);
@@ -79,13 +91,33 @@ public class ConversationItemFooter extends LinearLayout {
}
locationIndicatorView.setVisibility(messageRecord.hasLocation() ? View.VISIBLE : View.GONE);
presentDeliveryStatus(messageRecord);
boolean isOutChannel = DcHelper.getContext(context).getChat(messageRecord.getChatId()).isOutBroadcast();
if (isOutChannel && messageRecord.isOutgoing()) {
try {
int accId = rpc.getSelectedAccountId();
int count = rpc.getMessageReadReceiptCount(accId, messageRecord.getId());
viewsLabel.setText(String.format("%d", count));
viewsLabel.setVisibility(View.VISIBLE);
viewsIcon.setVisibility(View.VISIBLE);
} catch (RpcException e) {
e.printStackTrace();
}
} else {
viewsLabel.setVisibility(View.GONE);
viewsIcon.setVisibility(View.GONE);
}
presentDeliveryStatus(messageRecord, isOutChannel);
}
public void setTextColor(int color) {
textColor = color;
dateView.setTextColor(color);
editedView.setTextColor(color);
viewsLabel.setTextColor(color);
viewsIcon.setColorFilter(color);
bookmarkIndicatorView.setColorFilter(color);
emailIndicatorView.setColorFilter(color);
locationIndicatorView.setColorFilter(color);
@@ -106,7 +138,7 @@ public class ConversationItemFooter extends LinearLayout {
}
}
private void presentDeliveryStatus(@NonNull DcMsg messageRecord) {
private void presentDeliveryStatus(@NonNull DcMsg messageRecord, boolean isOutChannel) {
// isDownloading is temporary and should be checked first.
boolean isDownloading = messageRecord.getDownloadState() == DcMsg.DC_DOWNLOAD_IN_PROGRESS;
boolean isCall = messageRecord.getType() == DcMsg.DC_MSG_CALL;
@@ -114,17 +146,11 @@ public class ConversationItemFooter extends LinearLayout {
if (isDownloading) deliveryStatusView.setDownloading();
else if (messageRecord.isPending()) deliveryStatusView.setPending();
else if (messageRecord.isFailed()) deliveryStatusView.setFailed();
else if (!messageRecord.isOutgoing() || isCall) deliveryStatusView.setNone();
else if (!messageRecord.isOutgoing() || isCall || isOutChannel) deliveryStatusView.setNone();
else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead();
else if (messageRecord.isDelivered()) deliveryStatusView.setSent();
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
else deliveryStatusView.setPending();
if (messageRecord.isFailed()) {
deliveryStatusView.setTint(Color.RED);
} else {
deliveryStatusView.setTint(textColor); // Reset the color to the standard color (because the footer is re-used in a RecyclerView)
}
}
public String getDescription() {
@@ -11,6 +11,9 @@ import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import java.util.ArrayList;
import java.util.List;
public class RemovableEditableMediaView extends FrameLayout {
private final @NonNull ImageView remove;
@@ -19,6 +22,7 @@ public class RemovableEditableMediaView extends FrameLayout {
private final int removeSize;
private @Nullable View current;
private final List<OnClickListener> removeClickListeners = new ArrayList<>();
public RemovableEditableMediaView(Context context) {
this(context, null);
@@ -72,8 +76,22 @@ public class RemovableEditableMediaView extends FrameLayout {
return current;
}
public void setRemoveClickListener(View.OnClickListener listener) {
this.remove.setOnClickListener(listener);
public void addRemoveClickListener(View.OnClickListener listener) {
removeClickListeners.add(listener);
updateRemoveClickListener();
}
public void removeRemoveClickListener(View.OnClickListener listener) {
removeClickListeners.remove(listener);
updateRemoveClickListener();
}
private void updateRemoveClickListener() {
this.remove.setOnClickListener(v -> {
for (OnClickListener listener : removeClickListeners) {
listener.onClick(v);
}
});
}
public void setEditClickListener(View.OnClickListener listener) {
@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.net.Uri;
import androidx.annotation.Nullable;
public class AudioPlaybackState {
private final int msgId;
private final @Nullable Uri audioUri;
private final PlaybackStatus status;
private final long currentPosition;
private final long duration;
public enum PlaybackStatus {
IDLE,
LOADING,
PLAYING,
PAUSED,
ERROR
}
public AudioPlaybackState(int msgId,
@Nullable Uri audioUri,
PlaybackStatus status,
long currentPosition,
long duration) {
this.msgId = msgId;
this.audioUri = audioUri;
this.status = status;
this.currentPosition = currentPosition;
this.duration = duration;
}
public static AudioPlaybackState idle() {
return new AudioPlaybackState(0, null, PlaybackStatus.IDLE, 0, 0);
}
public int getMsgId() {
return msgId;
}
@Nullable
public Uri getAudioUri() {
return audioUri;
}
public PlaybackStatus getStatus() {
return status;
}
public long getCurrentPosition() {
return currentPosition;
}
public long getDuration() {
return duration;
}
}
@@ -0,0 +1,318 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.content.Context;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.session.MediaController;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AudioPlaybackViewModel extends ViewModel {
private static final String TAG = AudioPlaybackViewModel.class.getSimpleName();
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 Set<Integer> extractionInProgress = new HashSet<>();
private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2);
private @Nullable MediaController mediaController;
private final Handler handler;
private boolean isUserSeeking = false;
public AudioPlaybackViewModel() {
playbackState = new MutableLiveData<>(AudioPlaybackState.idle());
handler = new Handler(Looper.getMainLooper());
}
public LiveData<AudioPlaybackState> getPlaybackState() {
return playbackState;
}
public void setMediaController(@Nullable MediaController controller) {
this.mediaController = controller;
if (mediaController != null && mediaController.isPlaying()) {
startUpdateProgress();
}
updateCurrentState(true);
setupPlayerListener();
}
// Public methods
public void loadAudioAndPlay(int msgId, Uri audioUri) {
if (mediaController == null) return;
// Set media item if we have a different audio.
if (isDifferentAudio(msgId, audioUri)) {
updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0);
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(String.valueOf(msgId))
.setUri(audioUri)
.build();
mediaController.setMediaItem(mediaItem);
mediaController.prepare();
}
play(msgId, audioUri);
}
private boolean isSameAudio(int msgId, Uri audioUri) {
return !isDifferentAudio(msgId, audioUri);
}
private boolean isDifferentAudio(int msgId, Uri audioUri) {
AudioPlaybackState currentState = playbackState.getValue();
return currentState != null && (
msgId != currentState.getMsgId() ||
currentState.getAudioUri() == null ||
currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri));
}
public LiveData<Map<Integer, Long>> getDurations() {
return durations;
}
public void ensureDurationLoaded(Context context, int msgId, Uri audioUri) {
// Check cache
Map<Integer, Long> currentDurations = durations.getValue();
if (currentDurations != null && currentDurations.containsKey(msgId)) {
return;
}
// Check extracting
synchronized (extractionInProgress) {
if (extractionInProgress.contains(msgId)) {
return;
}
extractionInProgress.add(msgId);
}
// Extract in background
extractionExecutor.execute(() -> {
long duration = extractDurationFromAudio(context, audioUri);
handler.post(() -> {
Map<Integer, Long> updatedDurations = new HashMap<>(durations.getValue());
updatedDurations.put(msgId, duration);
durations.setValue(updatedDurations);
});
synchronized (extractionInProgress) {
extractionInProgress.remove(msgId);
}
});
}
private long extractDurationFromAudio(Context context, Uri audioUri) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setDataSource(context, audioUri);
String durationStr = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION
);
return durationStr != null ? Long.parseLong(durationStr) : 0;
} catch (Exception e) {
return 0;
} finally {
try {
retriever.release();
} catch (Exception ignored) {}
}
}
public void pause(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.pause();
}
}
public void play(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.play();
}
}
public void seekTo(long position, int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.seekTo(position);
}
}
public void stop(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
mediaController.stop();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
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) {
AudioPlaybackState currentState = playbackState.getValue();
if (mediaController != null && currentState != null) {
for (int msgId : msgIds) {
if (msgId == currentState.getMsgId()) {
mediaController.stop();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
}
}
public void setUserSeeking(boolean isUserSeeking) {
this.isUserSeeking = isUserSeeking;
}
// Private methods
private void setupPlayerListener() {
if (mediaController == null) return;
mediaController.addListener(new Player.Listener() {
@Override
public void onEvents(Player player, Player.Events events) {
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
if (player.isPlaying()) {
startUpdateProgress();
} else {
stopUpdateProgress();
}
updateCurrentState(false);
}
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
if (player.getPlaybackState() == Player.STATE_READY) {
updateCurrentState(false);
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
// This is to prevent automatically playing after the audio
// has been play to the end once, then user dragged the seek bar again
mediaController.setPlayWhenReady(false);
}
}
if (events.containsAny(Player.EVENT_PLAYER_ERROR)) {
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0);
}
}
});
}
private void updateCurrentState(boolean queryPlaying) {
if (mediaController == null) return;
AudioPlaybackState.PlaybackStatus status;
if (mediaController.isPlaying()) {
status = AudioPlaybackState.PlaybackStatus.PLAYING;
} else if (mediaController.getPlaybackState() == Player.STATE_READY
|| mediaController.getPlaybackState() == Player.STATE_ENDED) {
status = AudioPlaybackState.PlaybackStatus.PAUSED;
} else {
status = AudioPlaybackState.PlaybackStatus.IDLE;
}
Uri currentUri = null;
int currentMsgId = 0;
if (playbackState.getValue() != null) {
currentMsgId = playbackState.getValue().getMsgId();
currentUri = playbackState.getValue().getAudioUri();
}
if (queryPlaying || playbackState.getValue() == null) {
MediaItem item = mediaController.getCurrentMediaItem();
if (item != null) {
try {
currentMsgId = Integer.parseInt(item.mediaId);
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid integer", e);
}
if (item.localConfiguration != null) {
currentUri = item.localConfiguration.uri;
}
}
}
updateState(
currentMsgId,
currentUri,
status,
mediaController.getCurrentPosition(),
mediaController.getDuration());
}
private void updateState(int msgId,
Uri audioUri,
AudioPlaybackState.PlaybackStatus status,
long position,
long duration) {
// Sanitize longs
if (position < 0 || position > Integer.MAX_VALUE) {
position = 0;
}
if (duration < 0 || duration > Integer.MAX_VALUE) {
duration = 0;
}
playbackState.setValue(new AudioPlaybackState(
msgId, audioUri, status, position, duration
));
}
private void updateCurrentAudioState(AudioPlaybackState.PlaybackStatus status,
long position,
long duration) {
AudioPlaybackState current = playbackState.getValue();
if (current != null) {
updateState(current.getMsgId(), current.getAudioUri(), status, position, duration);
}
}
// Progress tracking
private final Runnable progressRunnable = new Runnable() {
@Override
public void run() {
if (mediaController != null && mediaController.isPlaying() && !isUserSeeking) {
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.PLAYING,
mediaController.getCurrentPosition(),
mediaController.getDuration());
handler.postDelayed(this, 100);
}
}
};
private void startUpdateProgress() {
stopUpdateProgress();
handler.post(progressRunnable);
}
private void stopUpdateProgress() {
handler.removeCallbacks(progressRunnable);
}
@Override
protected void onCleared() {
stopUpdateProgress();
extractionExecutor.shutdown();
super.onCleared();
}
}
@@ -0,0 +1,367 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.DateUtils;
import java.util.Map;
public class AudioView extends FrameLayout {
private static final String TAG = AudioView.class.getSimpleName();
private final @NonNull ImageView playPauseButton;
private final AnimatedVectorDrawableCompat playToPauseDrawable;
private final AnimatedVectorDrawableCompat pauseToPlayDrawable;
private final Drawable playDrawable;
private final Drawable pauseDrawable;
private final Animatable2Compat.AnimationCallback animationCallback;
private final @NonNull SeekBar seekBar;
private final @NonNull TextView timestamp;
private final @NonNull TextView title;
private final @NonNull View mask;
private OnActionListener listener;
private int msgId = -1;
private Uri audioUri;
private int progress;
private int duration;
private AudioPlaybackViewModel viewModel;
private final Observer<AudioPlaybackState> stateObserver = this::onPlaybackStateChanged;
private final Observer<Map<Integer, Long>> durationObserver = this::onDurationsChanged;
private boolean isPlaying;
public AudioView(Context context) {
this(context, null);
}
public AudioView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.audio_view, this);
this.playPauseButton = findViewById(R.id.play_pause);
this.seekBar = findViewById(R.id.seek);
this.timestamp = findViewById(R.id.timestamp);
this.title = findViewById(R.id.title);
this.mask = findViewById(R.id.interception_mask);
updateTimestampsAndSeekBar();
// Load drawables once
this.playToPauseDrawable = AnimatedVectorDrawableCompat.create(
getContext(), R.drawable.play_to_pause_animation);
this.pauseToPlayDrawable = AnimatedVectorDrawableCompat.create(
getContext(), R.drawable.pause_to_play_animation);
this.playDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.play_icon);
this.pauseDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.pause_icon);
this.animationCallback = new Animatable2Compat.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
Drawable endState = isPlaying ? pauseDrawable : playDrawable;
playPauseButton.setImageDrawable(endState);
}
};
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setupControls();
}
private void setupControls() {
// Set up observer in a very specific case when the view is detached and then re-attached,
// but binding from adapter has not happened yet
if (viewModel != null) {
viewModel.getPlaybackState().removeObserver(stateObserver);
viewModel.getPlaybackState().observeForever(stateObserver);
viewModel.getDurations().removeObserver(durationObserver);
viewModel.getDurations().observeForever(durationObserver);
}
playPauseButton.setOnClickListener(v -> {
Log.w(TAG, "playPauseButton onClick");
if (viewModel == null || audioUri == null) return;
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) {
// Same audio
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
viewModel.pause(msgId, audioUri);
} else {
viewModel.play(msgId, audioUri);
}
} else {
// Different audio
// Note: they can be the same *physical* file, but in different messages
viewModel.loadAudioAndPlay(msgId, audioUri);
}
if (listener != null) {
listener.onPlayPauseButtonClicked(v);
}
});
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
AudioView.this.progress = progress;
updateTimestampsAndSeekBar();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
viewModel.setUserSeeking(true);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
viewModel.setUserSeeking(false);
viewModel.seekTo(seekBar.getProgress(), msgId, audioUri);
}
});
if (playToPauseDrawable != null) {
playToPauseDrawable.registerAnimationCallback(animationCallback);
}
if (pauseToPlayDrawable != null) {
pauseToPlayDrawable.registerAnimationCallback(animationCallback);
}
}
@Override
protected void onDetachedFromWindow() {
if (viewModel != null) {
viewModel.getPlaybackState().removeObserver(stateObserver);
viewModel.getDurations().removeObserver(durationObserver);
}
if (playToPauseDrawable != null) {
playToPauseDrawable.clearAnimationCallbacks();
}
if (pauseToPlayDrawable != null) {
pauseToPlayDrawable.clearAnimationCallbacks();
}
super.onDetachedFromWindow();
}
public void setPlaybackViewModel(AudioPlaybackViewModel viewModel) {
if (this.viewModel != null) {
this.viewModel.getPlaybackState().removeObserver(stateObserver);
this.viewModel.getDurations().removeObserver(durationObserver);
}
// ViewModel is used directly for simplicity, since there is no reuse yet
this.viewModel = viewModel;
if (viewModel != null) {
viewModel.getPlaybackState().observeForever(stateObserver);
viewModel.getDurations().observeForever(durationObserver);
}
}
public void setAudio(final @NonNull AudioSlide audio)
{
msgId = audio.getDcMsgId();
audioUri = audio.getUri();
playPauseButton.setImageDrawable(playDrawable);
seekBar.setEnabled(true);
// 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);
}
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
title.setVisibility(View.GONE);
}
else {
title.setText(audio.getFileName().get());
title.setVisibility(View.VISIBLE);
}
}
@Override
public void setOnClickListener(OnClickListener listener) {
super.setOnClickListener(listener);
this.mask.setOnClickListener(listener);
}
@Override
public void setOnLongClickListener(OnLongClickListener listener) {
super.setOnLongClickListener(listener);
this.mask.setOnLongClickListener(listener);
this.playPauseButton.setOnLongClickListener(listener);
}
public int getMsgId() {
return msgId;
}
public Uri getAudioUri() {
return audioUri;
}
public interface OnActionListener {
void onPlayPauseButtonClicked(View view);
}
public void setOnActionListener(OnActionListener listener) {
this.listener = listener;
}
public void togglePlay() {
playPauseButton.performClick();
}
public String getDescription() {
String desc;
if (this.title.getVisibility() == View.VISIBLE) {
desc = getContext().getString(R.string.audio);
} else {
desc = getContext().getString(R.string.voice_message);
}
desc += "\n" + this.timestamp.getText();
if (title.getVisibility() == View.VISIBLE) {
desc += "\n" + this.title.getText();
}
return desc;
}
private void updateProgress(AudioPlaybackState state) {
int duration = Math.toIntExact(state.getDuration());
int position = Math.toIntExact(state.getCurrentPosition());
if (duration > 0) {
this.progress = position;
this.duration = duration;
updateTimestampsAndSeekBar();
}
}
public void disablePlayer(boolean disable) {
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
}
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
seekBar.getGlobalVisibleRect(rect);
}
private void togglePlayPause(boolean expectedPlaying) {
isPlaying = expectedPlaying;
Drawable expectedDrawable = expectedPlaying ? pauseDrawable : playDrawable;
boolean isAnimating = false;
Drawable currentDrawable = playPauseButton.getDrawable();
if (currentDrawable instanceof AnimatedVectorDrawableCompat) {
isAnimating = ((AnimatedVectorDrawableCompat) currentDrawable).isRunning();
}
if (!isAnimating && playPauseButton.getDrawable() != expectedDrawable) {
AnimatedVectorDrawableCompat animDrawable = expectedPlaying ? playToPauseDrawable : pauseToPlayDrawable;
String contentDescription = getContext().getString(
expectedPlaying ? R.string.menu_pause : R.string.menu_play);
if (animDrawable != null) {
playPauseButton.setImageDrawable(animDrawable);
playPauseButton.setContentDescription(contentDescription);
animDrawable.start();
}
}
}
private void onPlaybackStateChanged(AudioPlaybackState state) {
if (audioUri == null || state == null) return;
// Check if this state is about this message
boolean isThisMessage = msgId == state.getMsgId() && audioUri.equals(state.getAudioUri());
if (isThisMessage) {
updateUIForPlaybackState(state);
} else {
togglePlayPause(false);
// Also clear progress to avoid confusion
this.progress = 0;
updateTimestampsAndSeekBar();
}
}
private void updateUIForPlaybackState(AudioPlaybackState state) {
switch (state.getStatus()) {
case PLAYING:
togglePlayPause(true);
updateProgress(state);
break;
case PAUSED:
togglePlayPause(false);
updateProgress(state);
break;
case LOADING:
case ERROR:
// No special handling yet
break;
}
}
private void onDurationsChanged(Map<Integer, Long> durations) {
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
// When there is no playback happening, msgId can be -1 and audioUri is null
if (state != null &&
msgId >= 0 && msgId == state.getMsgId() &&
audioUri != null && audioUri.equals(state.getAudioUri())) {
return; // Is playing this message
}
Long duration = durations.get(msgId);
if (duration != null && seekBar.getMax() <= 100) {
this.duration = Math.toIntExact(duration);
updateTimestampsAndSeekBar();
seekBar.setMax(this.duration);
}
}
private void updateTimestampsAndSeekBar() {
String progressText = DateUtils.getFormatedDuration(progress);
String durationText = DateUtils.getFormatedDuration(duration);
timestamp.setText(String.format("%s / %s", progressText, durationText));
seekBar.setProgress(progress);
seekBar.setMax(duration);
}
}
@@ -5,7 +5,6 @@ import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.PreferenceManager;
@@ -14,7 +13,6 @@ import com.b44t.messenger.DcContext;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.InstantOnboardingActivity;
import org.thoughtcrime.securesms.WelcomeActivity;
import org.thoughtcrime.securesms.accounts.AccountSelectionListFragment;
@@ -123,10 +121,6 @@ public class AccountManager {
}
public void switchAccountAndStartActivity(Activity activity, int destAccountId) {
switchAccountAndStartActivity(activity, destAccountId, null);
}
private void switchAccountAndStartActivity(Activity activity, int destAccountId, @Nullable String backupQr) {
if (destAccountId==0) {
beginAccountCreation(activity);
} else {
@@ -134,12 +128,8 @@ public class AccountManager {
}
activity.finishAffinity();
if (destAccountId==0) {
Intent intent = new Intent(activity, WelcomeActivity.class);
if (backupQr != null) {
intent.putExtra(WelcomeActivity.BACKUP_QR_EXTRA, backupQr);
}
activity.startActivity(intent);
if (destAccountId == 0) {
activity.startActivity(new Intent(activity, WelcomeActivity.class));
} else {
activity.startActivity(new Intent(activity.getApplicationContext(), ConversationListActivity.class));
}
@@ -147,12 +137,21 @@ public class AccountManager {
// ui
public void showSwitchAccountMenu(ConversationListActivity activity) {
AccountSelectionListFragment dialog = new AccountSelectionListFragment(activity);
public void showSwitchAccountMenu(ConversationListActivity activity, boolean selectOnly) {
AccountSelectionListFragment dialog = AccountSelectionListFragment.newInstance(selectOnly);
dialog.show(((FragmentActivity) activity).getSupportFragmentManager(), null);
}
public void addAccountFromSecondDevice(Activity activity, String backupQr) {
switchAccountAndStartActivity(activity, 0, backupQr);
DcAccounts accounts = DcHelper.getAccounts(activity);
if (accounts.getSelectedAccount().isConfigured() == 1) {
// the selected account is already configured, create a new one
beginAccountCreation(activity);
}
activity.finishAffinity();
Intent intent = new Intent(activity, WelcomeActivity.class);
intent.putExtra(WelcomeActivity.BACKUP_QR_EXTRA, backupQr);
activity.startActivity(intent);
}
}
@@ -164,6 +164,23 @@ public class DcEventCenter {
});
}
public void handleLogging(@NonNull DcEvent event) {
final String logPrefix = "[accId="+event.getAccountId()+ "] ";
switch (event.getId()) {
case DcContext.DC_EVENT_INFO:
Log.i("DeltaChat", logPrefix + event.getData2Str());
break;
case DcContext.DC_EVENT_WARNING:
Log.w("DeltaChat", logPrefix + event.getData2Str());
break;
case DcContext.DC_EVENT_ERROR:
Log.e("DeltaChat", logPrefix + event.getData2Str());
break;
}
}
public long handleEvent(@NonNull DcEvent event) {
int accountId = event.getAccountId();
int id = event.getId();
@@ -205,20 +222,7 @@ public class DcEventCenter {
return 0;
}
final String logPrefix = "[accId="+accountId + "] ";
switch (id) {
case DcContext.DC_EVENT_INFO:
Log.i("DeltaChat", logPrefix + event.getData2Str());
break;
case DcContext.DC_EVENT_WARNING:
Log.w("DeltaChat", logPrefix + event.getData2Str());
break;
case DcContext.DC_EVENT_ERROR:
Log.e("DeltaChat", logPrefix + event.getData2Str());
break;
}
handleLogging(event);
if (accountId != context.getDcContext().getAccountId()) {
return 0;
@@ -132,7 +132,7 @@ public class DcHelper {
dcContext.setStockTranslation(68, context.getString(R.string.device_talk));
dcContext.setStockTranslation(69, context.getString(R.string.saved_messages));
dcContext.setStockTranslation(70, context.getString(R.string.device_talk_explain));
dcContext.setStockTranslation(71, context.getString(R.string.device_welcome_message, "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=MVPi07rQBEmHO4FRb3brpwDe&j=n8mkKqu42WAKKUCx1bQOVh23&s=RxuXoa0vhvTs0QLsWM45Ues0&a=adb%40arcanechat.me&n=adb&b=ArcaneChat+Channel"));
dcContext.setStockTranslation(71, context.getString(R.string.device_welcome_message, "https://i.delta.chat/#0A45953086F0C166D3BAF1D4BB2025496E4C2704&x=3KvvQZfzU4t-9u5s0PF3USGp&i=X-QDZ681F6Plz_uBu47CKdg4&s=IwE4LraLDcdPBW597wB7DBnI&a=arcanechat%40arcanechat.me&n=ArcaneChat&g=ArcaneChat+Community"));
dcContext.setStockTranslation(73, context.getString(R.string.systemmsg_subject_for_new_contact));
dcContext.setStockTranslation(74, context.getString(R.string.systemmsg_failed_sending_to));
dcContext.setStockTranslation(84, context.getString(R.string.configuration_failed_with_error));
@@ -210,8 +210,6 @@ public class DcHelper {
dcContext.setStockTranslation(178, context.getString(R.string.member_x_removed));
dcContext.setStockTranslation(190, context.getString(R.string.secure_join_wait));
dcContext.setStockTranslation(193, context.getString(R.string.donate_device_msg));
dcContext.setStockTranslation(194, context.getString(R.string.outgoing_call));
dcContext.setStockTranslation(195, context.getString(R.string.incoming_call));
dcContext.setStockTranslation(196, context.getString(R.string.declined_call));
dcContext.setStockTranslation(197, context.getString(R.string.canceled_call));
dcContext.setStockTranslation(198, context.getString(R.string.missed_call));
@@ -223,6 +221,12 @@ public class DcHelper {
dcContext.setStockTranslation(220, context.getString(R.string.proxy_enabled));
dcContext.setStockTranslation(221, context.getString(R.string.proxy_enabled_hint));
dcContext.setStockTranslation(230, context.getString(R.string.chat_unencrypted_explanation));
dcContext.setStockTranslation(232, context.getString(R.string.outgoing_audio_call));
dcContext.setStockTranslation(233, context.getString(R.string.outgoing_video_call));
dcContext.setStockTranslation(234, context.getString(R.string.incoming_audio_call));
dcContext.setStockTranslation(235, context.getString(R.string.incoming_video_call));
dcContext.setStockTranslation(240, context.getString(R.string.chat_description_changed_by_you));
dcContext.setStockTranslation(241, context.getString(R.string.chat_description_changed_by_other));
}
public static File getImexDir() {
@@ -101,6 +101,10 @@ public class ThreadRecord {
return spannable;
}
public boolean isOutChannel() {
return recipient.getChat() != null && recipient.getChat().isOutBroadcast();
}
public int getUnreadCount() {
return unreadCount;
}
@@ -49,13 +49,13 @@ import org.thoughtcrime.securesms.WebxdcActivity;
import org.thoughtcrime.securesms.WebxdcStoreActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.VcardView;
import org.thoughtcrime.securesms.components.WebxdcView;
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.DcLocationManager;
@@ -121,7 +121,7 @@ public class AttachmentManager {
//this.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
removableMediaView.addRemoveClickListener(new RemoveButtonListener());
removableMediaView.setEditClickListener(new EditButtonListener());
thumbnail.setOnClickListener(new ThumbnailClickListener());
}
@@ -152,8 +152,6 @@ public class AttachmentManager {
markGarbage(getSlideUri());
slide = Optional.absent();
audioView.cleanup();
}
}
@@ -235,7 +233,8 @@ public class AttachmentManager {
@NonNull final MediaType mediaType,
final int width,
final int height,
final int chatId)
final int chatId,
AudioPlaybackViewModel playbackViewModel)
{
inflateStub();
@@ -285,26 +284,12 @@ public class AttachmentManager {
setAttachmentPresent(true);
if (slide.hasAudio()) {
class SetDurationListener implements AudioSlidePlayer.Listener {
@Override
public void onStart() {}
@Override
public void onStop() {}
@Override
public void onProgress(AudioSlide slide, double progress, long millis) {}
@Override
public void onReceivedDuration(int millis) {
((AudioView) removableMediaView.getCurrent()).setDuration(millis);
}
}
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(context, (AudioSlide) slide, new SetDurationListener());
audioSlidePlayer.requestDuration();
audioView.setAudio((AudioSlide) slide, 0);
audioView.setPlaybackViewModel(playbackViewModel);
audioView.setAudio((AudioSlide) slide);
removableMediaView.display(audioView, false);
removableMediaView.addRemoveClickListener(v -> {
playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri());
});
result.set(true);
} else if (slide.isVcard()) {
vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context));
@@ -61,6 +61,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import chat.delta.rpc.RpcException;
public class NotificationCenter {
private static final String TAG = NotificationCenter.class.getSimpleName();
@NonNull private final ApplicationContext context;
@@ -164,7 +166,7 @@ public class NotificationCenter {
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
}
public PendingIntent getOpenCallIntent(ChatData chatData, int callId, String payload, boolean autoAccept) {
public PendingIntent getOpenCallIntent(ChatData chatData, int callId, String payload, boolean autoAccept, boolean hasVideo) {
final Intent chatIntent = new Intent(context, ConversationActivity.class)
.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, chatData.accountId)
.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatData.chatId)
@@ -184,6 +186,7 @@ public class NotificationCenter {
intent.putExtra(CallActivity.EXTRA_CHAT_ID, chatData.chatId);
intent.putExtra(CallActivity.EXTRA_CALL_ID, callId);
intent.putExtra(CallActivity.EXTRA_HASH, hash);
intent.putExtra(CallActivity.EXTRA_HAS_VIDEO, hasVideo);
intent.setPackage(context.getPackageName());
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(chatIntent)
@@ -427,6 +430,13 @@ public class NotificationCenter {
Util.runOnAnyBackgroundThread(() -> {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
DcContext dcContext = context.getDcAccounts().getAccount(accId);
boolean hasVideo;
try {
hasVideo = context.getRpc().callInfo(accId, callId).hasVideo;
} catch (RpcException e) {
Log.e(TAG, "Rpc.callInfo() failed", e);
hasVideo = false;
}
int chatId = dcContext.getMsg(callId).getChatId();
DcChat dcChat = dcContext.getChat(chatId);
String name = dcChat.getName();
@@ -434,7 +444,7 @@ public class NotificationCenter {
String notificationChannel = getCallNotificationChannel(notificationManager, chatData, name);
PendingIntent declineCallIntent = getDeclineCallIntent(chatData, callId);
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false);
PendingIntent openCallIntent = getOpenCallIntent(chatData, callId, payload, false, hasVideo);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel)
.setSmallIcon(R.drawable.icon_notification)
@@ -459,7 +469,7 @@ public class NotificationCenter {
new NotificationCompat.Action.Builder(
R.drawable.ic_videocam_white_24dp,
context.getString(R.string.answer_call),
getOpenCallIntent(chatData, callId, payload, true)).build());
getOpenCallIntent(chatData, callId, payload, true, hasVideo)).build());
Bitmap bitmap = getAvatar(dcChat);
if (bitmap != null) {
@@ -21,7 +21,7 @@ import java.io.IOException;
public class AvatarHelper {
/* the maximum width/height an avatar should have */
public static final int AVATAR_SIZE = 640;
public static final int AVATAR_SIZE = 512;
public static void setGroupAvatar(Context context, int chatId, Bitmap bitmap) {
DcContext dcContext = DcHelper.getContext(context);
@@ -11,6 +11,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
@@ -56,6 +57,8 @@ public class BackupTransferActivity extends BaseActionBarActivity {
private boolean notificationControllerClosed = false;
public boolean warnAboutCopiedQrCodeOnAbort = false;
private OnBackPressedCallback backPressedCallback;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -89,6 +92,15 @@ public class BackupTransferActivity extends BaseActionBarActivity {
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(findViewById(R.id.backup_provider_fragment));
backPressedCallback = new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
finishOrAskToFinish();
}
};
getOnBackPressedDispatcher().addCallback(this, backPressedCallback);
}
@Override
@@ -107,18 +119,13 @@ public class BackupTransferActivity extends BaseActionBarActivity {
return super.onPrepareOptionsMenu(menu);
}
@Override
public void onBackPressed() {
finishOrAskToFinish();
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finishOrAskToFinish();
getOnBackPressedDispatcher().onBackPressed();
return true;
} else if (itemId == R.id.troubleshooting) {
DcHelper.openHelp(this, "#multiclient");
@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.qr;
import android.app.Activity;
import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.CaptureManager;
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
public class CustomCaptureManager extends CaptureManager {
private OnResultInterceptor interceptor;
public CustomCaptureManager(Activity activity, DecoratedBarcodeView barcodeView) {
super(activity, barcodeView);
}
public void setResultInterceptor(OnResultInterceptor interceptor) {
this.interceptor = interceptor;
}
@Override
protected void returnResult(BarcodeResult rawResult) {
if (interceptor != null) {
interceptor.onResult(rawResult, () -> {
super.returnResult(rawResult);
});
} else {
super.returnResult(rawResult);
}
}
public interface OnResultInterceptor {
void onResult(BarcodeResult result, Runnable finishCallback);
}
}
@@ -58,7 +58,9 @@ public class QrCodeHandler {
public void handleOnlySecureJoinQr(String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) {
final DcLot qrParsed = dcContext.checkQr(rawString);
if (!handleSecureJoinQr(qrParsed, rawString, source, uiPath)) {
handleDefault(new AlertDialog.Builder(activity), rawString, qrParsed);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
handleDefault(builder, rawString, qrParsed);
builder.create().show();
}
}
@@ -122,7 +124,9 @@ public class QrCodeHandler {
public void handleOnlyAddRelayQr(String rawString) {
final DcLot qrParsed = dcContext.checkQr(rawString);
if (!handleAddRelayQr(qrParsed, rawString)) {
handleDefault(new AlertDialog.Builder(activity), rawString, qrParsed);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
handleDefault(builder, rawString, qrParsed);
builder.create().show();
}
}
@@ -321,25 +325,29 @@ public class QrCodeHandler {
}
builder.setMessage(msg);
builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
try {
int newChatId = DcHelper.getRpc(activity).secureJoinWithUxInfo(dcContext.getAccountId(), qrRawString, source, uipath);
if (newChatId == 0) throw new Exception("Securejoin failed to create a chat");
Intent intent = new Intent(activity, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, newChatId);
activity.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder1 = new AlertDialog.Builder(activity);
builder1.setMessage(e.getMessage());
builder1.setPositiveButton(android.R.string.ok, null);
builder1.create().show();
}
secureJoinByQr(qrRawString, source, uipath);
});
builder.setNegativeButton(android.R.string.cancel, null);
}
public void addRelay(String qrData) {
public void secureJoinByQr(String qrRawString, SecurejoinSource source, SecurejoinUiPath uipath) {
try {
int newChatId = DcHelper.getRpc(activity).secureJoinWithUxInfo(dcContext.getAccountId(), qrRawString, source, uipath);
if (newChatId == 0) throw new Exception("Securejoin failed to create a chat");
Intent intent = new Intent(activity, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, newChatId);
activity.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
AlertDialog.Builder builder1 = new AlertDialog.Builder(activity);
builder1.setMessage(e.getMessage());
builder1.setPositiveButton(android.R.string.ok, null);
builder1.create().show();
}
}
public void addRelay(String qrData) {
ProgressDialog progressDialog = new ProgressDialog(activity);
progressDialog.setMessage(activity.getResources().getString(R.string.one_moment));
progressDialog.setCanceledOnTouchOutside(false);
@@ -10,8 +10,12 @@ import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.journeyapps.barcodescanner.CaptureManager;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcLot;
import com.journeyapps.barcodescanner.CompoundBarcodeView;
import org.thoughtcrime.securesms.BaseActionBarActivity;
@@ -26,10 +30,12 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
public static final String ADD_AS_SECOND_DEVICE_EXTRA = "add_as_second_device";
public static final String QRDATA_EXTRA = "qrdata";
private CaptureManager capture;
private CustomCaptureManager capture;
private CompoundBarcodeView barcodeScannerView;
private DcContext dcContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -66,6 +72,8 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
.onAnyResult(this::handleQrScanWithPermissions)
.onAnyDenied(this::handleQrScanWithDeniedPermission)
.execute();
dcContext = DcHelper.getContext(this);
}
private void handleQrScanWithPermissions() {
@@ -89,23 +97,70 @@ public class RegistrationQrActivity extends BaseActionBarActivity {
DcHelper.openHelp(this, "#multiclient");
return true;
} else if (itemId == R.id.menu_paste) {
Intent intent = new Intent();
intent.putExtra(QRDATA_EXTRA, Util.getTextFromClipboard(this));
setResult(Activity.RESULT_OK, intent);
finish();
String rawQr = Util.getTextFromClipboard(this);
Runnable okCallback = () -> {
Intent intent = new Intent();
intent.putExtra(QRDATA_EXTRA, rawQr);
setResult(Activity.RESULT_OK, intent);
finish();
};
showConfirmDialog(rawQr, okCallback, null);
return true;
}
return false;
}
@Override
private void showConfirmDialog(String rawQr, @NonNull Runnable okCallback, @Nullable Runnable cancelCallback) {
DcLot qrParsed = dcContext.checkQr(rawQr);
String dialogMsg = "";
if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT) {
String name = dcContext.getContact(qrParsed.getId()).getDisplayName();
dialogMsg = getString(R.string.instant_onboarding_confirm_contact, name);
} else if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) {
String groupName = qrParsed.getText1();
dialogMsg = getString(R.string.instant_onboarding_confirm_group, groupName);
}
if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT
|| qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) {
AlertDialog confirmDialog = new AlertDialog.Builder(this)
.setMessage(dialogMsg)
.setPositiveButton("OK", (dialog, which) -> {
okCallback.run();
})
.setNegativeButton("Cancel", (dialog, which) -> {
if (cancelCallback != null) {
cancelCallback.run();
}
})
.show();
} else {
okCallback.run();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void init(CompoundBarcodeView barcodeScannerView, Intent intent, Bundle savedInstanceState) {
capture = new CaptureManager(this, barcodeScannerView);
capture = new CustomCaptureManager(this, barcodeScannerView);
capture.setResultInterceptor((result, finishCallback) -> {
String rawQr = result.getText();
showConfirmDialog(rawQr, finishCallback, () -> {
barcodeScannerView.resume();
capture.decode();
});
});
capture.initializeFromIntent(intent, savedInstanceState);
capture.decode();
}
@@ -36,19 +36,24 @@ import chat.delta.rpc.types.Reactions;
public class ReactionsDetailsFragment extends DialogFragment implements DcEventCenter.DcEventDelegate {
private static final String TAG = ReactionsDetailsFragment.class.getSimpleName();
private static final String ARG_MSG_ID = "msg_id";
private RecyclerView recyclerView;
private ReactionRecipientsAdapter adapter;
private final int msgId;
private int msgId;
public ReactionsDetailsFragment(int msgId) {
super();
this.msgId = msgId;
public static ReactionsDetailsFragment newInstance(int msgId) {
ReactionsDetailsFragment fragment = new ReactionsDetailsFragment();
Bundle args = new Bundle();
args.putInt(ARG_MSG_ID, msgId);
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
msgId = getArguments() != null? getArguments().getInt(ARG_MSG_ID, 0) : 0;
adapter = new ReactionRecipientsAdapter(requireActivity(), GlideApp.with(requireActivity()), new ListClickListener());
LayoutInflater inflater = requireActivity().getLayoutInflater();
@@ -3,7 +3,9 @@ package org.thoughtcrime.securesms.relay;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
@@ -47,6 +49,9 @@ public class RelayListActivity extends BaseActionBarActivity
/** QR provided via Intent extras needs to be saved to pass it to QrCodeHandler when authorization finishes */
private String qrData = null;
/** Relay selected for context menu via onRelayLongClick() */
private EnteredLoginParam contextMenuRelay = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -143,14 +148,54 @@ public class RelayListActivity extends BaseActionBarActivity
}
@Override
public void onRelayEdit(EnteredLoginParam relay) {
public void onRelayLongClick(View view, EnteredLoginParam relay) {
contextMenuRelay = relay;
registerForContextMenu(view);
openContextMenu(view);
unregisterForContextMenu(view);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.relay_item_context, menu);
boolean nonNullAddr = contextMenuRelay != null && contextMenuRelay.addr != null;
boolean isMain = nonNullAddr && contextMenuRelay.addr.equals(adapter.getMainRelay());
menu.findItem(R.id.menu_delete_relay).setVisible(!isMain);
}
@Override
public void onContextMenuClosed(android.view.Menu menu) {
super.onContextMenuClosed(menu);
contextMenuRelay = null;
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
if (contextMenuRelay == null) return super.onContextItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.menu_edit_relay) {
onRelayEdit(contextMenuRelay);
contextMenuRelay = null;
return true;
} else if (itemId == R.id.menu_delete_relay) {
onRelayDelete(contextMenuRelay);
contextMenuRelay = null;
return true;
}
return super.onContextItemSelected(item);
}
private void onRelayEdit(EnteredLoginParam relay) {
Intent intent = new Intent(this, EditRelayActivity.class);
intent.putExtra(EditRelayActivity.EXTRA_ADDR, relay.addr);
startActivity(intent);
}
@Override
public void onRelayDelete(EnteredLoginParam relay) {
private void onRelayDelete(EnteredLoginParam relay) {
new AlertDialog.Builder(this)
.setTitle(R.string.remove_transport)
.setMessage(getString(R.string.confirm_remove_transport, relay.addr))
@@ -25,8 +25,7 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
public interface OnRelayClickListener {
void onRelayClick(EnteredLoginParam relay);
void onRelayEdit(EnteredLoginParam relay);
void onRelayDelete(EnteredLoginParam relay);
void onRelayLongClick(View view, EnteredLoginParam relay);
}
public RelayListAdapter(OnRelayClickListener listener) {
@@ -67,16 +66,12 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
private final TextView titleText;
private final TextView subtitleText;
private final ImageView mainIndicator;
private final ImageView editButton;
private final ImageView deleteButton;
public RelayViewHolder(@NonNull View itemView) {
super(itemView);
titleText = itemView.findViewById(R.id.title);
subtitleText = itemView.findViewById(R.id.subtitle);
mainIndicator = itemView.findViewById(R.id.main_indicator);
editButton = itemView.findViewById(R.id.edit_button);
deleteButton = itemView.findViewById(R.id.delete_button);
}
public void bind(EnteredLoginParam relay, boolean isMain, OnRelayClickListener listener) {
@@ -84,7 +79,6 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
titleText.setText(parts.length == 2? parts[1] : parts[0]);
subtitleText.setText(parts.length == 2? parts[0] : "");
mainIndicator.setVisibility(isMain ? View.VISIBLE : View.INVISIBLE);
deleteButton.setVisibility(isMain ? View.GONE : View.VISIBLE);
itemView.setOnClickListener(v -> {
if (listener != null) {
@@ -92,16 +86,11 @@ public class RelayListAdapter extends RecyclerView.Adapter<RelayListAdapter.Rela
}
});
editButton.setOnClickListener(v -> {
itemView.setOnLongClickListener(v -> {
if (listener != null) {
listener.onRelayEdit(relay);
}
});
deleteButton.setOnClickListener(v -> {
if (listener != null) {
listener.onRelayDelete(relay);
listener.onRelayLongClick(v, relay);
}
return true;
});
}
}
@@ -27,11 +27,4 @@ public class ScribbleActivity extends PassphraseRequiredActionBarActivity {
boolean cropAvatar = getIntent().getBooleanExtra(CROP_AVATAR, false);
imageEditorFragment = initFragment(R.id.scribble_container, ImageEditorFragment.newInstance(getIntent().getData(), cropAvatar));
}
/* @Override
public void onBackPressed() {
if (!imageEditorFragment.onBackPressed()) {
super.onBackPressed();
}
} */
}
@@ -63,7 +63,7 @@ public class StickerSelectActivity extends FragmentActivity implements StickerSe
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
getOnBackPressedDispatcher().onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
@@ -0,0 +1,140 @@
package org.thoughtcrime.securesms.service;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.MediaSession;
import androidx.media3.session.MediaSessionService;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionCommands;
import androidx.media3.session.SessionResult;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.ConversationListActivity;
public class AudioPlaybackService extends MediaSessionService {
private static final String TAG = AudioPlaybackService.class.getSimpleName();
private ExoPlayer player;
private MediaSession session;
@Override
public void onCreate() {
super.onCreate();
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA) // USAGE_VOICE_COMMUNICATION is for VoIP calls
.setContentType(C.AUDIO_CONTENT_TYPE_SPEECH)
.build();
player = new ExoPlayer.Builder(this)
.setAudioAttributes(audioAttributes, true)
.setHandleAudioBecomingNoisy(true)
.build();
// This is for click on the notification to go back to app
Intent intent = new Intent(this, ConversationListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent initialIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
session = new MediaSession.Builder(this, player)
.setSessionActivity(initialIntent)
.setCallback(new MediaSession.Callback() {
@OptIn(markerClass = UnstableApi.class)
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session,
MediaSession.ControllerInfo controller
) {
SessionCommands sessionCommands = MediaSession
.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
.add(new SessionCommand("UPDATE_ACTIVITY_CONTEXT", new Bundle()))
.build();
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands)
.build();
}
@NonNull
@Override
public ListenableFuture<SessionResult> onCustomCommand(
MediaSession session,
MediaSession.ControllerInfo controller,
SessionCommand customCommand,
Bundle args
) {
if ("UPDATE_ACTIVITY_CONTEXT".equals(customCommand.customAction)) {
updateSessionActivity(args);
}
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_SUCCESS));
}
})
.build();
}
@OptIn(markerClass = UnstableApi.class)
private void updateSessionActivity(Bundle args) {
try {
// Put all the original extras back into the intent
if (args != null && !args.isEmpty()) {
String activityClassName = args.getString("activity_class");
args.remove("activity_class");
if (activityClassName != null) {
Class<?> activityClass = Class.forName(activityClassName);
Intent intent = new Intent(this, activityClass);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtras(args);
PendingIntent pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
session.setSessionActivity(pendingIntent);
}
}
} catch (ClassNotFoundException e) {
Log.e(TAG, "Activity class not found", e);
}
}
@Nullable
@Override
public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
return session;
}
@Override
public void onDestroy() {
if (session != null) {
session.release();
session = null;
}
if (player != null) {
player.release();
player = null;
}
super.onDestroy();
}
}
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.util;
import static org.thoughtcrime.securesms.util.ShareUtil.getForwardedMessageAccountId;
import static org.thoughtcrime.securesms.util.ShareUtil.getForwardedMessageIDs;
import static org.thoughtcrime.securesms.util.ShareUtil.getSharedText;
import static org.thoughtcrime.securesms.util.ShareUtil.getSharedUris;
@@ -29,6 +30,11 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
public class SendRelayedMessageUtil {
private static final String TAG = SendRelayedMessageUtil.class.getSimpleName();
@@ -40,12 +46,27 @@ public class SendRelayedMessageUtil {
public static void immediatelyRelay(Activity activity, final Long[] chatIds) {
ConversationListRelayingActivity.finishActivity();
if (isForwarding(activity)) {
int forwardedMsgAccId = getForwardedMessageAccountId(activity);
int[] forwardedMessageIDs = getForwardedMessageIDs(activity);
resetRelayingMessageContent(activity);
if (forwardedMessageIDs == null) return;
if (forwardedMessageIDs == null || forwardedMsgAccId <= 0) return;
Util.runOnAnyBackgroundThread(() -> {
DcContext dcContext = DcHelper.getContext(activity);
int accId = dcContext.getAccountId();
if (forwardedMsgAccId != accId) {
Rpc rpc = DcHelper.getRpc(activity);
List<Integer> list = Util.toList(forwardedMessageIDs);
for (long longChatId : chatIds) {
try {
rpc.forwardMessagesToAccount(forwardedMsgAccId, list, accId, (int)longChatId);
} catch (RpcException e) {
e.printStackTrace();
}
}
return;
}
for (long longChatId : chatIds) {
int chatId = (int) longChatId;
if (dcContext.getChat(chatId).isSelfTalk()) {
@@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
import java.util.ArrayList;
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";
@@ -27,11 +28,7 @@ public class ShareUtil {
}
public static boolean isForwarding(Activity activity) {
try {
return activity.getIntent().getIntArrayExtra(FORWARDED_MESSAGE_IDS) != null;
} catch (NullPointerException npe) {
return false;
}
return getForwardedMessageAccountId(activity) > 0;
}
public static boolean isSharing(Activity activity) {
@@ -66,6 +63,14 @@ public class ShareUtil {
}
}
public static int getForwardedMessageAccountId(Activity activity) {
try {
return activity.getIntent().getIntExtra(FORWARDED_MESSAGE_ACCID, -1);
} catch (NullPointerException npe) {
return -1;
}
}
static int[] getForwardedMessageIDs(Activity activity) {
try {
return activity.getIntent().getIntArrayExtra(FORWARDED_MESSAGE_IDS);
@@ -137,6 +142,7 @@ public class ShareUtil {
public static void resetRelayingMessageContent(Activity activity) {
try {
activity.getIntent().removeExtra(FORWARDED_MESSAGE_ACCID);
activity.getIntent().removeExtra(FORWARDED_MESSAGE_IDS);
activity.getIntent().removeExtra(SHARED_URIS);
activity.getIntent().removeExtra(SHARED_CONTACT_ID);
@@ -153,7 +159,8 @@ public class ShareUtil {
public static void acquireRelayMessageContent(Activity currentActivity, @NonNull Intent newActivityIntent) {
if (isForwarding(currentActivity)) {
newActivityIntent.putExtra(FORWARDED_MESSAGE_IDS, getForwardedMessageIDs(currentActivity));
int accId = getForwardedMessageAccountId(currentActivity);
setForwardingMessageIds(newActivityIntent, getForwardedMessageIDs(currentActivity), accId);
} else if (isSharing(currentActivity)) {
newActivityIntent.putExtra(IS_SHARING, true);
if (isDirectSharing(currentActivity)) {
@@ -180,7 +187,8 @@ public class ShareUtil {
}
}
public static void setForwardingMessageIds(Intent composeIntent, int[] messageIds) {
public static void setForwardingMessageIds(Intent composeIntent, int[] messageIds, int accId) {
composeIntent.putExtra(FORWARDED_MESSAGE_ACCID, accId);
composeIntent.putExtra(FORWARDED_MESSAGE_IDS, messageIds);
}
@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import androidx.core.text.BidiFormatter;
public class TextUtil {
@NonNull
public static String bidiAppend(String text, String suffix) {
return BidiFormatter.getInstance().unicodeWrap(text) + suffix;
}
@NonNull
public static String markAsExternal(String text) {
return bidiAppend(text, "");
}
}
@@ -53,7 +53,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
@@ -115,6 +117,14 @@ public class Util {
}
}
public static List<Integer> toList(int[] array) {
ArrayList<Integer> list = new ArrayList<>(array.length);
for (int i : array) {
list.add(i);
}
return list;
}
public static @NonNull int[] appendInt(@Nullable int[] cur, int val) {
if (cur == null) {
return new int[] { val };
@@ -323,7 +323,15 @@ public class ViewUtil {
* @param view The view to apply insets to
*/
public static void applyWindowInsetsAsMargin(@NonNull View view) {
applyWindowInsetsAsMargin(view, true, true, true, true);
applyWindowInsetsAsMargin(view, true, true, true, true, false);
}
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsetsAsMargin(view, left, top, right, bottom, false);
}
public static void forceApplyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsetsAsMargin(view, left, top, right, bottom, true);
}
/**
@@ -337,8 +345,9 @@ public class ViewUtil {
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
* @param forceDispatch Whether to force application of insets
*/
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean forceDispatch) {
// Only enable on API 30+ where WindowInsets APIs work correctly
if (!isEdgeToEdgeSupported()) return;
@@ -371,10 +380,10 @@ public class ViewUtil {
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) layoutParams;
marginParams.leftMargin = baseMarginLeft + insets.left;
marginParams.topMargin = baseMarginTop + insets.top;
marginParams.rightMargin = baseMarginRight + insets.right;
marginParams.bottomMargin = baseMarginBottom + insets.bottom;
marginParams.leftMargin = left ? baseMarginLeft + insets.left : baseMarginLeft;
marginParams.topMargin = top ? baseMarginTop + insets.top : baseMarginTop;
marginParams.rightMargin = right ? baseMarginRight + insets.right : baseMarginRight;
marginParams.bottomMargin = bottom ? baseMarginBottom + insets.bottom : baseMarginBottom;
v.setLayoutParams(marginParams);
}
@@ -383,18 +392,54 @@ public class ViewUtil {
// Request the initial insets to be dispatched if the view is attached
if (view.isAttachedToWindow()) {
ViewCompat.requestApplyInsets(view);
if (forceDispatch) {
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
if (insets != null) {
ViewCompat.dispatchApplyWindowInsets(view, insets);
}
} else {
ViewCompat.requestApplyInsets(view);
}
}
}
/**
* Apply window insets to a view by adding padding to avoid drawing elements behind system bars.
* Convenience method that applies insets to all sides.
* IME insets are propagated to child views.
*
* @param view The view to apply insets to
*/
public static void applyWindowInsets(@NonNull View view) {
applyWindowInsets(view, true, true, true, true);
applyWindowInsets(view, true, true, true, true, false, false);
}
/**
* Apply window insets to a view by adding padding to avoid drawing elements behind system bars.
*
* IME insets are propagated to child views.
*
* @param view The view to apply insets to
* @param left Whether to apply left inset
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
*/
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsets(view, left, top, right, bottom, false, false);
}
/**
* Force applying window insets to a view by adding padding to avoid drawing elements behind system bars.
*
* @param view The view to apply insets to
* @param left Whether to apply left inset
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
*/
public static void forceApplyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
applyWindowInsets(view, left, top, right, bottom, false, true);
}
/**
@@ -408,8 +453,10 @@ public class ViewUtil {
* @param top Whether to apply top inset
* @param right Whether to apply right inset
* @param bottom Whether to apply bottom inset
* @param consumeImeInsets Whether to consume IME insets so they don't propagate to child views
* @param forceDispatch Force application of Insets, regardless if system think it shall dispatch
*/
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean consumeImeInsets, boolean forceDispatch) {
// Only enable on API 30+ where WindowInsets APIs work correctly
if (!isEdgeToEdgeSupported()) return;
@@ -442,12 +489,24 @@ public class ViewUtil {
bottom ? basePaddingBottom + insets.bottom : basePaddingBottom
);
if (consumeImeInsets) {
windowInsets = new WindowInsetsCompat.Builder(windowInsets)
.setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE)
.build();
}
return windowInsets;
});
// Request the initial insets to be dispatched if the view is attached
if (view.isAttachedToWindow()) {
ViewCompat.requestApplyInsets(view);
if (forceDispatch) {
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
if (insets != null) {
ViewCompat.dispatchApplyWindowInsets(view, insets);
}
} else {
ViewCompat.requestApplyInsets(view);
}
}
}

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