Compare commits

...

343 Commits
quote ... trunk

Author SHA1 Message Date
mati 1e4b597e34 Updated Polish translation 2021-12-25 12:22:04 +01:00
mati 3998d39e6d Updated Polish translation 2021-12-25 12:19:51 +01:00
Sarah Jamie Lewis 66aa9ef00d Merge pull request 'fileCPU' (#276) from fileCPU into trunk
Reviewed-on: cwtch.im/cwtch-ui#276
2021-12-21 19:56:11 +00:00
Sarah Jamie Lewis 3af3142c67 Merge branch 'trunk' into fileCPU 2021-12-21 19:56:05 +00:00
Sarah Jamie Lewis 8f02ce7560 Merge pull request 'don't pop nav stack to profileMgr when change password errors' (#275) from chPasswordErr into trunk
Reviewed-on: cwtch.im/cwtch-ui#275
2021-12-21 18:49:05 +00:00
Dan Ballard 37700e1a78 move the CheckDownloadStatus to filemessage model and only do to init 2021-12-21 13:29:20 -05:00
Dan Ballard adc6c0491b comment out CheckDownloadStatus loop in filebubble 2021-12-21 10:13:43 -05:00
Dan Ballard 7434cf0934 don't pop nav stack to profileMgr when change password errors 2021-12-21 09:18:21 -05:00
Sarah Jamie Lewis af91288bb7 Merge pull request 'theme fixes; scroollbar for addContact; messageRow space optimization for android' (#274) from alignments into trunk
Reviewed-on: cwtch.im/cwtch-ui#274
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-12-21 08:51:44 +00:00
Sarah Jamie Lewis 1f28490eb9 Merge branch 'trunk' into alignments 2021-12-21 08:44:50 +00:00
Dan Ballard da9d093a13 theme fixes; scroollbar for addContact; messageRow space optimization for android 2021-12-21 00:07:16 -05:00
Sarah Jamie Lewis 41de498548 Merge pull request 'lastFixes' (#273) from lastFixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#273
2021-12-21 00:11:45 +00:00
Dan Ballard 5f21ebc2b2 lokalise translation updates 2021-12-20 19:04:11 -05:00
Dan Ballard 607a602e3c libcwtch 1.5.2 and drone no download if have non essential containers 2021-12-20 19:00:11 -05:00
Dan Ballard 513035789d add mouse icon over file previews to trigger dialog; add exit button to image preview dialoge; spacing 2021-12-20 16:34:51 -05:00
Dan Ballard 936fca943a don't show server metrics when not running (undefined anyway); properly define hilight border around empty message pane in dual pane mode 2021-12-20 14:33:34 -05:00
Dan Ballard 36cf8abc8c Merge pull request '1.5-upgrades' (#272) from 1.5-upgrades into trunk
Reviewed-on: cwtch.im/cwtch-ui#272
2021-12-19 23:04:17 +00:00
Sarah Jamie Lewis 6928ff562b Bump Minor 2021-12-19 14:13:04 -08:00
Sarah Jamie Lewis 3fde1a0de0 Upgrade Cwtch Versions 2021-12-19 13:20:06 -08:00
Sarah Jamie Lewis a6743af103 IndexFailure + Android Change Password Fixups 2021-12-19 12:45:13 -08:00
Sarah Jamie Lewis d13ded8f50 Merge pull request 'image previews' (#267) from ipreview into trunk
Reviewed-on: cwtch.im/cwtch-ui#267
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-12-19 20:05:08 +00:00
Sarah Jamie Lewis b0cf3d2021 Merge branch 'trunk' into ipreview 2021-12-19 20:04:30 +00:00
Sarah Jamie Lewis ebc8a740d3 Merge pull request 'remove RightShiftFixer for winodws, flutter should be better now; rm tri[lecolview, unused half a year' (#271) from rmRSF into trunk
Reviewed-on: cwtch.im/cwtch-ui#271
2021-12-19 20:04:19 +00:00
Dan Ballard d4b614f2c4 remove RightShiftFixer for winodws, flutter should be better now; rm tri[lecolview, unused half a year 2021-12-19 14:04:58 -05:00
Dan Ballard 92dea31a86 drone update to use new flutter 2.8 containers 2021-12-18 23:32:52 -05:00
erinn d2d07c81e3 bump lcg 2021-12-18 18:44:22 -08:00
erinn 09496713d9 merge trunk 2021-12-18 18:39:07 -08:00
erinn 6390cb55a4 l10n 2021-12-18 18:09:18 -08:00
erinn 81d43554b5 Merge branch 'ipreview' of git.openprivacy.ca:cwtch.im/cwtch-ui into ipreview 2021-12-18 18:03:10 -08:00
erinn 26c84472d8 l10n 2021-12-18 18:02:44 -08:00
erinn a36a55c53f Merge branch 'ipreview' of git.openprivacy.ca:cwtch.im/cwtch-ui into ipreview 2021-12-18 17:43:39 -08:00
erinn d329e7cc06 image previews - dan comments - wip 2021-12-18 17:43:32 -08:00
Sarah Jamie Lewis 1f376c7ed1 Merge pull request 'migrateMsg' (#266) from migrateMsg into trunk
Reviewed-on: cwtch.im/cwtch-ui#266
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-12-18 21:54:59 +00:00
Sarah Jamie Lewis e0a28b4dc1 Merge branch 'trunk' into migrateMsg 2021-12-18 21:28:37 +00:00
Sarah Jamie Lewis ffb518b47d Merge pull request 'group functionality fixes from storage engine; tor status include percent; crate group enhancement' (#265) from groupFixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#265
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-12-18 21:28:28 +00:00
Sarah Jamie Lewis 79cfd9f7fa Merge branch 'trunk' into groupFixes 2021-12-18 21:28:21 +00:00
Sarah Jamie Lewis bb1ebb7468 Merge branch 'trunk' into migrateMsg 2021-12-18 21:26:01 +00:00
Sarah Jamie Lewis 924bfa4352 Merge pull request 'winUninstall' (#268) from winUninstall into trunk
Reviewed-on: cwtch.im/cwtch-ui#268
2021-12-18 21:25:45 +00:00
Sarah Jamie Lewis 293bf34f91 Merge branch 'trunk' into winUninstall 2021-12-18 21:25:36 +00:00
Dan Ballard 8e489131fa fix bg color of servers pane 2021-12-18 13:51:52 -05:00
Dan Ballard 4946b6c51a fix nsis syntax; write uninstaller 2021-12-18 11:51:44 -05:00
Sarah Jamie Lewis 78265b7f85 Merge branch 'ipreview' of git.openprivacy.ca:cwtch.im/cwtch-ui into ipreview 2021-12-17 17:05:59 -08:00
Sarah Jamie Lewis 845ce3b546 Scaling + Android ChangePassword 2021-12-17 17:02:24 -08:00
Sarah Jamie Lewis 6f3d5b65cd preview 2021-12-17 16:54:30 -08:00
erinn c78dbf0ac3 fileshare confirmations and contact-add-gating 2021-12-17 16:32:22 -08:00
Dan Ballard 6adea075e7 Make splash scren stateful; add translations; add message and progress bar for storage migration 2021-12-17 18:23:18 -05:00
Dan Ballard 75acfe8dd0 contact list sort more often and float unauthed contacts to top 2021-12-17 17:34:56 -05:00
Dan Ballard 6ba447f941 appState support modal message 2021-12-17 17:13:09 -05:00
erinn 9cb6eb857d Merge branch 'ipreview' of git.openprivacy.ca:cwtch.im/cwtch-ui into ipreview 2021-12-16 17:04:35 -08:00
erinn 6e9fb6810e message previews 2021-12-16 17:04:29 -08:00
Sarah Jamie Lewis df3617c5a1 Fixup Preview Dialog 2021-12-16 16:54:53 -08:00
Dan Ballard eb38f48c67 first pass at uninstaller 2021-12-16 19:52:15 -05:00
Dan Ballard aff7a5f4b2 deleting removed event event handlers 2021-12-16 19:50:43 -05:00
Sarah Jamie Lewis 8a8c5fc3e2 Update Langs 2021-12-16 12:52:25 -08:00
Dan Ballard cde6962b8e group functionality fixes from storage engine; tor status include percent; crate group enhancement 2021-12-16 13:36:14 -05:00
Sarah Jamie Lewis 0ef219bbb6 Merge pull request 'New Themes' (#260) from themes into trunk
Reviewed-on: cwtch.im/cwtch-ui#260
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-12-15 22:30:39 +00:00
Dan Ballard abf4d79e80 flutter format 2021-12-15 17:29:27 -05:00
Sarah Jamie Lewis a4cb5051b5 Fixing Duplicate Key Issues + FileBubble SetState Builder Issues 2021-12-15 13:45:33 -08:00
Dan Ballard b29bb1e4dc clean up comments, delete old code, pr comments 2021-12-15 15:28:49 -05:00
Dan Ballard 124694447b i10n theme names 2021-12-15 15:22:17 -05:00
Dan Ballard 8af4deaf95 the rest of the themes 2021-12-15 15:22:17 -05:00
Dan Ballard e599768643 update with new PRs rebase, update goldens for slight variance 2021-12-15 15:22:17 -05:00
Dan Ballard dc550daaa1 migrate to new libcwtch changes; add vampire and witch; tweak switch button coloring 2021-12-15 15:22:17 -05:00
Dan Ballard 089fee4c41 updates 2021-12-15 15:22:17 -05:00
Dan Ballard 53e79f5b9d refactor themes from methods to get fields; add theme selector; add neon1 2021-12-15 15:22:17 -05:00
Dan Ballard 194ade9aa6 refactor light and dark onto existing new color scheme; fix message editor top strip color; tweak color field semantics 2021-12-15 15:22:17 -05:00
Dan Ballard f6a4d5c3fa removing unused theme definitions 2021-12-15 15:22:17 -05:00
Sarah Jamie Lewis 9a7ce2afc7 Merge pull request 'support for server metrics' (#259) from serverMetrics into trunk
Reviewed-on: cwtch.im/cwtch-ui#259
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-12-15 20:21:44 +00:00
Dan Ballard 3b893d4f15 gate profile servers on groups or servers experiment 2021-12-15 15:20:07 -05:00
erinn 51fae2e7ef wip image previews 2021-12-14 17:25:05 -08:00
erinn fc488bdb2e Merge branch 'ipreview' of git.openprivacy.ca:cwtch.im/cwtch-ui into ipreview 2021-12-14 17:19:40 -08:00
erinn e026b2d542 l10n image previews 2021-12-14 17:19:13 -08:00
erinn a0fc096f6f Merge branch 'ipreview' of git.openprivacy.ca:cwtch.im/cwtch-ui into ipreview 2021-12-14 17:13:19 -08:00
erinn b994c42803 wip image previews 2021-12-14 17:13:13 -08:00
erinn c16800bc0c image previews merge 2021-12-14 17:13:04 -08:00
erinn 56e9e36074 l10n image previews 2021-12-14 17:12:16 -08:00
erinn 8c4a5aee90 wip image previews 2021-12-14 15:50:08 -08:00
Sarah Jamie Lewis 8be0fa3957 File Bubble Design 2021-12-14 15:37:27 -08:00
erinn 63fc1fe772 Merge branch 'trunk' of git.openprivacy.ca:cwtch.im/cwtch-ui into ipreview 2021-12-14 13:37:48 -08:00
erinn 33a8a30170 wip image previews 2021-12-14 13:33:30 -08:00
Dan Ballard 2d66e42624 support for server metrics 2021-12-14 10:29:19 -06:00
erinn 62b70713fb Merge pull request 'Fix #255 - clicking Open now causes popup to close.' (#262) from linkfixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#262
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-12-14 00:53:32 +00:00
Sarah Jamie Lewis 816425cd4d Fix #255 - clicking Open now causes popup to close.
Also importing new translatable strings + a few nicer URL options
2021-12-13 15:42:42 -08:00
Sarah Jamie Lewis 71a25f5c5d Merge pull request 'Profile server manager' (#256) from profServers into trunk
Reviewed-on: cwtch.im/cwtch-ui#256
2021-12-13 19:46:32 +00:00
Dan Ballard 63ed835880 remove commented out code 2021-12-10 23:26:47 -08:00
Dan Ballard 2257a63e17 new libcwtch 2021-12-10 23:14:42 -08:00
Dan Ballard 6180b88172 port to new cwtch storage API 2021-12-10 15:37:08 -08:00
Dan Ballard 3f5428eff8 complete profile level server managment 2021-12-10 14:49:38 -08:00
Dan Ballard 0c797faf05 profile level server list and editor start 2021-12-10 14:45:13 -08:00
Dan Ballard 1d5359e645 start of profile server manager 2021-12-10 14:41:48 -08:00
Sarah Jamie Lewis 1d884cab9d Merge pull request 'Update Russian Translations' (#246) from russian into trunk
Reviewed-on: cwtch.im/cwtch-ui#246
2021-12-10 21:03:00 +00:00
Sarah Jamie Lewis 405c4aeb4c Merge branch 'trunk' into russian 2021-12-10 21:02:46 +00:00
Sarah Jamie Lewis 18e3ab7bc8 Merge pull request 'New Cwtch Library Integration' (#258) from cwtch-lib-integration into trunk
Reviewed-on: cwtch.im/cwtch-ui#258
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-12-10 21:02:01 +00:00
Sarah Jamie Lewis 066b4d4dec Erinn Comment Fixups 2021-12-10 11:51:19 -08:00
Sarah Jamie Lewis c5c0f21829 Fix New Marker. Add Placeholder for Import Handling 2021-12-08 16:41:01 -08:00
Sarah Jamie Lewis 30fa788b84 Combine update cache and total messages increase. Remove CachedMessage 2021-12-08 14:39:14 -08:00
Sarah Jamie Lewis db05c78106 Merge branch 'trunk' into cwtch-lib-integration 2021-12-08 05:48:29 +00:00
Sarah Jamie Lewis 6b4ccd401e Flutter Upgrades 2021-12-07 21:47:28 -08:00
Sarah Jamie Lewis 35d09a5207 Upgrade LibCwtch-go 2021-12-07 21:46:39 -08:00
Sarah Jamie Lewis 995282fa04 Fixup Acks / Message Keys 2021-12-06 13:42:40 -08:00
Sarah Jamie Lewis c9319d32d0 Much improved message caching 2021-12-06 12:26:02 -08:00
Sarah Jamie Lewis c42be6224d Porting Android over to new API 2021-12-03 11:28:10 -08:00
Sarah Jamie Lewis d6839c62e3 Move Attribute Updates / File Downloading / Contact Requests / Invites to new API 2021-12-01 04:17:48 -08:00
Sarah Jamie Lewis b0f74ffb6d Fixup Conversation Attribute Flow 2021-11-26 15:07:37 -08:00
Sarah Jamie Lewis c00bfbb48b Quoted Message Fixes / New Message Label Offet Fixes 2021-11-26 14:25:21 -08:00
Sarah Jamie Lewis f030e8b573 Merge pull request 'readme-run' (#249) from readme-run into trunk
Reviewed-on: cwtch.im/cwtch-ui#249
2021-11-26 01:17:04 +00:00
Sarah Jamie Lewis 880c1c107b UI Updates to new Cwtch API 2021-11-25 15:59:54 -08:00
Dan Ballard 3ed8b94274 Merge branch 'trunk' into readme-run 2021-11-24 01:01:06 +00:00
Sarah Jamie Lewis 1d6b533df3 New Cwtch Library Integration 2021-11-18 15:44:54 -08:00
Sarah Jamie Lewis 2c1347e50e Merge pull request 'Clickable hyperlinks for MessageBubbles' (#235) from NimaBoscarino/cwtch-ui:nima/clickable-links into trunk
Reviewed-on: cwtch.im/cwtch-ui#235
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-11-17 02:09:06 +00:00
Sarah Jamie Lewis c4edd9fc67 Merge branch 'trunk' into russian 2021-11-17 01:29:36 +00:00
Dan Ballard 40c27c3f30 Merge branch 'trunk' into nima/clickable-links 2021-11-17 01:27:17 +00:00
Sarah Jamie Lewis d69966423a Merge pull request 'bumping docker containers to flutter stable 2.5.3' (#250) from droneBump into trunk
Reviewed-on: cwtch.im/cwtch-ui#250
2021-11-17 01:26:36 +00:00
Dan Ballard 883b739b5c bumping docker containers to flutter stable 2.5.3 2021-11-16 16:34:28 -08:00
Dan Ballard 1d024ac63e fix mac run instructions and make clear when running debug or release builds 2021-11-16 15:08:09 -08:00
Dan Ballard b34ffcd211 update README with windows run in place instructions 2021-11-16 15:03:18 -08:00
Sarah Jamie Lewis fec22538d2 Merge branch 'trunk' into russian 2021-11-11 01:17:46 +00:00
Sarah Jamie Lewis 90df697a69 Merge pull request 'small_fix-Russian Localization' (#243) from RuLang/cwtch-ui-rulang:trunk into russian
Reviewed-on: cwtch.im/cwtch-ui#243
2021-11-11 01:03:59 +00:00
Sarah Jamie Lewis 4dea1e1dd4 Merge branch 'trunk' into nima/clickable-links 2021-11-11 00:22:53 +00:00
erinn 6a5309427f Merge pull request 'scrolling fixes' (#244) from scrollfixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#244
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-11-11 00:20:01 +00:00
erinn 6d261c999b use l10n str 2021-11-10 16:19:18 -08:00
erinn 00ce310cbc l10n update 2021-11-10 16:04:51 -08:00
erinn 946d7accb2 Merge branch 'trunk' of git.openprivacy.ca:cwtch.im/cwtch-ui into scrollfixes 2021-11-10 15:52:42 -08:00
erinn 996cad4ece autoscroll fixes 2021-11-10 15:52:34 -08:00
RuLang 51d98b5171 Update Russian Localization 2021-11-10 19:30:31 +00:00
Sarah Jamie Lewis b9549e6885 Merge branch 'trunk' into nima/clickable-links 2021-11-10 18:13:15 +00:00
Dan Ballard e01cea3238 Merge pull request 'Russian Localization' (#242) from russian into trunk
Reviewed-on: cwtch.im/cwtch-ui#242
2021-11-10 18:09:28 +00:00
Sarah Jamie Lewis d553d6a474 Merge branch 'trunk' into nima/clickable-links 2021-11-10 18:03:36 +00:00
Sarah Jamie Lewis d0770b4bd8 Russian Localization 2021-11-10 09:58:46 -08:00
Sarah Jamie Lewis 07b97fdb02 Merge pull request 'libcwtch version bump (fix windows servers crash)' (#241) from lcgVBump into trunk
Reviewed-on: cwtch.im/cwtch-ui#241
2021-11-09 23:50:02 +00:00
Dan Ballard ff28f37471 libcwtch version bump (fix windows servers crash) 2021-11-09 15:36:46 -08:00
Nima Boscarino 1c03fdf1db Add link styling, using main text colour 2021-11-08 12:57:55 -08:00
Nima Boscarino a58e09dec5 android config for url_launcher 2021-11-07 00:56:48 -07:00
Nima Boscarino 35dcc24e66 Remove resolved TODO statements, and destroy modal
after copying link
2021-11-06 10:53:42 -07:00
Nima Boscarino b8d50f234a Merge branch 'trunk' into nima/clickable-links 2021-11-05 22:39:51 -07:00
Nima Boscarino ec1dd05ba1 WIP: add experimental clickable links with dialog
(copy / open). Bug remaining for selectable text
2021-11-05 22:38:45 -07:00
Sarah Jamie Lewis edfc070d9a Merge pull request 'libcwtch go bump and server fixes' (#234) from macServerFix into trunk
Reviewed-on: cwtch.im/cwtch-ui#234
2021-11-06 00:36:45 +00:00
Dan Ballard d3da68272b libcwtch go bump (fix join on add serverer); swap deploy folder name parts date/ver 2021-11-05 17:23:45 -07:00
Dan Ballard 44b77f0a90 libcwtch go bump (fix create server on mac), copy keys text fix 2021-11-05 13:45:50 -07:00
Sarah Jamie Lewis 10584984c3 Merge pull request 'Upgrade libCwtch to v1.4.0' (#233) from 1.4.0-release into trunk
Reviewed-on: cwtch.im/cwtch-ui#233
2021-11-05 17:07:03 +00:00
Sarah Jamie Lewis 0d4c67923d Upgrade libCwtch to v1.4.0 2021-11-04 22:34:06 -07:00
Dan Ballard e9044daab3 Merge pull request 'macos drone build' (#232) from macosBuild into trunk
Reviewed-on: cwtch.im/cwtch-ui#232
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-11-05 04:57:28 +00:00
Dan Ballard 659b8d5bf1 macos drone build 2021-11-04 20:27:03 -07:00
Sarah Jamie Lewis 0dc4849a5d Merge pull request 'download resumption, hash verification' (#231) from filefix2 into trunk
Reviewed-on: cwtch.im/cwtch-ui#231
2021-11-05 00:08:56 +00:00
erinn 626aa386d0 update lcg version 2021-11-04 16:31:01 -07:00
erinn 8653a31482 Merge branch 'l10nup' of git.openprivacy.ca:cwtch.im/cwtch-ui into filefix2 2021-11-04 15:44:49 -07:00
erinn 10838ba67a l10n updates for file resumption 2 2021-11-04 15:44:31 -07:00
erinn d581718d36 Merge branch 'trunk' of git.openprivacy.ca:cwtch.im/cwtch-ui into filefix2 2021-11-04 15:31:56 -07:00
erinn e55d9301e4 file resumption support 2021-11-04 15:31:50 -07:00
Sarah Jamie Lewis f7f2ca6621 Merge pull request 'l10n updates for file resumption' (#230) from l10nup into trunk
Reviewed-on: cwtch.im/cwtch-ui#230
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-11-04 22:07:48 +00:00
erinn 6331a6da95 l10n updates for file resumption 2021-11-04 15:02:26 -07:00
Dan Ballard 9fe7ee4b56 Merge pull request 'server delete functionality and minor cleanups' (#229) from serverDelete into trunk
Reviewed-on: cwtch.im/cwtch-ui#229
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-11-04 18:49:31 +00:00
Dan Ballard fb18a0e3b8 fix serverliststate add to handle readds (happens on saving global settings) 2021-11-04 11:49:03 -07:00
Dan Ballard 87f7c2c29b server delete functionality and minor cleanups 2021-11-03 21:31:03 -07:00
Sarah Jamie Lewis 07b00ee1d0 Merge pull request 'lcgVBump and restrict mobile on servers' (#228) from lcgVBump into trunk
Reviewed-on: cwtch.im/cwtch-ui#228
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-11-03 20:17:29 +00:00
Sarah Jamie Lewis 05433397d4 Merge branch 'trunk' into lcgVBump 2021-11-03 20:00:29 +00:00
Dan Ballard fc59bd332b restruct mobile from servers experiment and tab 2021-11-03 12:53:32 -07:00
Dan Ballard 3ac8c32afe libcwtch bump 2021-11-03 12:44:09 -07:00
Dan Ballard 2c57499478 Merge pull request 'group_message_fixes' (#227) from group_message_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#227
2021-11-03 19:33:09 +00:00
Dan Ballard a9bcf13c1f Merge pull request 'Servers experiment and manager' (#214) from server into trunk
Reviewed-on: cwtch.im/cwtch-ui#214
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-11-02 22:26:09 +00:00
Dan Ballard 82294eb6af proper tooltips 2021-11-02 15:19:16 -07:00
Dan Ballard 60a4775f4d libcwtch bump 2021-11-02 15:13:12 -07:00
Dan Ballard 3e75c4d106 last cleanup 2021-11-02 15:10:36 -07:00
Dan Ballard e9407ae426 fixing as per PR 2021-11-02 14:48:52 -07:00
Sarah Jamie Lewis c5f154a25f Fmt 2021-11-02 13:47:52 -07:00
Sarah Jamie Lewis cae44cbd46 Ensure unique keys for messages 2021-11-02 13:47:52 -07:00
Dan Ballard b65d16aa8a translations 2021-11-02 13:36:59 -07:00
Dan Ballard ed8292ece9 serverlist check password. also libcwtch-go version 2021-11-01 22:16:52 -07:00
Dan Ballard 562c05183b server list, add edit 2021-11-01 19:45:27 -07:00
Dan Ballard c304e2ec2a server list, add edit 2021-11-01 19:45:27 -07:00
Dan Ballard 9789a42e94 server manager 2021-11-01 19:45:27 -07:00
Dan Ballard d4b9f1dc55 lib/cwtch new servers api coverage and Set[Profile/Contact]Attribute 2021-11-01 19:45:26 -07:00
erinn 2b8f8e825f Merge pull request 'Rely on Index from NewGroupMessage to sync Timelines' (#225) from group_message_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#225
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-11-01 23:03:51 +00:00
Sarah Jamie Lewis 73690e8bac Combined if statements 2021-11-01 15:15:30 -07:00
Sarah Jamie Lewis 088cebcc6b Merge branch 'trunk' into group_message_fixes 2021-11-01 22:08:12 +00:00
Sarah Jamie Lewis fe156108b5 Rely on Index from NewGroupMessage to sync Timelines 2021-11-01 15:07:09 -07:00
Dan Ballard 4971879b0e Merge pull request 'pubspec version code bump' (#204) from psvd into trunk
Reviewed-on: cwtch.im/cwtch-ui#204
2021-10-02 17:32:13 +00:00
Dan Ballard 9403a1c540 pubspec version code bump 2021-10-02 10:31:04 -07:00
Sarah Jamie Lewis d4811407b2 Merge pull request 'remove unneeded permissions' (#198) from filefix into trunk
Reviewed-on: cwtch.im/cwtch-ui#198
2021-10-01 20:55:45 +00:00
erinn c2297d5b5d remove unneeded permissions 2021-10-01 13:53:09 -07:00
Sarah Jamie Lewis 10197dc300 Merge pull request 'fix filesharing on android' (#197) from filefix into trunk
Reviewed-on: cwtch.im/cwtch-ui#197
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-10-01 19:48:01 +00:00
erinn 368515423b restore android button 2021-10-01 12:39:33 -07:00
erinn 74e25534c5 android file sending bugfix 2021-10-01 12:38:06 -07:00
Sarah Jamie Lewis e36f04095f Merge pull request 'pubspec version bump' (#195) from pubver into trunk
Reviewed-on: cwtch.im/cwtch-ui#195
2021-10-01 15:46:59 +00:00
Dan Ballard c174f2f293 pubspec version bump 2021-10-01 08:45:20 -07:00
Dan Ballard d070b7141e Merge pull request 'Prevent Sharing Files on Android for Now' (#194) from android-files into trunk
Reviewed-on: cwtch.im/cwtch-ui#194
2021-10-01 15:05:36 +00:00
Sarah Jamie Lewis 0cdf18df79 fmt 2021-10-01 07:57:29 -07:00
Sarah Jamie Lewis 222e2c6f36 Prevent Sharing Files on Android for Now 2021-10-01 07:52:49 -07:00
Dan Ballard e9514f0296 Merge pull request 'Upgrade libcwtch-go' (#193) from windows-fix into trunk
Reviewed-on: cwtch.im/cwtch-ui#193
2021-09-30 23:31:59 +00:00
Sarah Jamie Lewis 175bfefa4e Merge branch 'trunk' into windows-fix 2021-09-30 23:22:31 +00:00
Sarah Jamie Lewis 77fdecc748 Upgrade libcwtch-go 2021-09-30 16:21:36 -07:00
Sarah Jamie Lewis 0c81b4f9d0 Merge pull request 'filesharing' (#188) from filesharing into trunk
Reviewed-on: cwtch.im/cwtch-ui#188
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-09-30 20:39:46 +00:00
Sarah Jamie Lewis 9556150c05 Upgrade libCwtch-go + Fix Model merge issue 2021-09-30 13:29:52 -07:00
Sarah Jamie Lewis efbf7f5bff Small file sharing cleanup 2021-09-30 10:53:32 -07:00
Sarah Jamie Lewis 3240e41d49 Upgrade libCwtch-go 2021-09-30 10:50:27 -07:00
Sarah Jamie Lewis bf31a2b062 Filesharing UI Updates 2021-09-29 17:29:14 -07:00
erinn d8453bc530 Merge branch 'trunk' into filesharing 2021-09-30 00:21:27 +00:00
erinn 01d816209b flutter format 2021-09-29 17:20:35 -07:00
erinn a9cc4b7425 revert accidental add 2021-09-29 17:18:53 -07:00
erinn d4aa1cb397 android download notification, also fix updatemessageflags didnt work on android 2021-09-29 17:16:00 -07:00
erinn 8fe577afd4 moar fileshare plz 2021-09-29 13:31:01 -07:00
Sarah Jamie Lewis b972cfd45e Merge pull request 'add bunny streamer icon to settings' (#187) from bunny into trunk
Reviewed-on: cwtch.im/cwtch-ui#187
2021-09-29 20:19:18 +00:00
Dan Ballard 6d79c1dc17 add bunny streamer icon to settings 2021-09-28 16:44:50 -07:00
erinn cb3c161277 wip: filesharing ui dev 2021-09-27 12:53:21 -07:00
erinn 4eed72ded3 Merge branch 'trunk' of git.openprivacy.ca:cwtch.im/cwtch-ui into filesharing 2021-09-21 14:57:48 -07:00
erinn 78ea12dff3 filesharing wip 2021-09-21 14:57:40 -07:00
Sarah Jamie Lewis ac4e0c1679 Merge pull request 'l10n updates for file sharing' (#186) from l10n into trunk
Reviewed-on: cwtch.im/cwtch-ui#186
2021-09-21 21:48:37 +00:00
erinn 4f614b69fd l10n updates 2021-09-21 14:47:31 -07:00
erinn e7c6bb145a l10n updates 2021-09-21 14:45:31 -07:00
erinn 77ea122ff3 Merge pull request 'Light Theme Fixes #162' (#185) from theme-fix into trunk
Reviewed-on: cwtch.im/cwtch-ui#185
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-09-21 20:47:21 +00:00
Sarah Jamie Lewis affc48089d Merge branch 'trunk' into theme-fix 2021-09-21 20:41:37 +00:00
Sarah Jamie Lewis 539b93836a Light Theme Fixes #162 2021-09-21 13:31:11 -07:00
Dan Ballard 6b33e129be Merge pull request 'Fix #163' (#184) from reply-fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#184
2021-09-17 20:55:29 +00:00
Sarah Jamie Lewis c1aee0d128 Fix #163
Also make quoted messages nicer
2021-09-17 13:38:10 -07:00
Sarah Jamie Lewis 3a34044f8e Merge pull request 'streamerMode' (#183) from streamerMode into trunk
Reviewed-on: cwtch.im/cwtch-ui#183
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-09-14 23:36:17 +00:00
Dan Ballard be2de14d65 make settings null safer, l10n 2021-09-14 14:38:38 -07:00
Dan Ballard ac619cd514 make settings null safer 2021-09-14 13:48:25 -07:00
Dan Ballard 34da2bea35 streamer mode 2021-09-14 09:05:07 -07:00
Sarah Jamie Lewis f3b09d3e3a Merge pull request 'macosBits' (#180) from macosBits into trunk
Reviewed-on: cwtch.im/cwtch-ui#180
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-09-11 01:27:57 +00:00
Dan Ballard 7de7e36e99 macos: handle shutdown & first release run copy dev profile if none 2021-09-10 18:12:30 -07:00
Dan Ballard c172a814bf .drone.yml: update all flutter images to 2.5, change win tor file to op buildfiles mirror 2021-09-09 11:15:13 -07:00
Dan Ballard afe448b43e Merge pull request 'readme: clarify build instructions' (#167) from mal/cwtch-ui:readme-details into trunk
Reviewed-on: cwtch.im/cwtch-ui#167
2021-09-09 18:12:45 +00:00
Trevor Bergeron d85aac0a13
readme: clarify build instructions 2021-09-09 02:13:46 -04:00
Dan Ballard 4c46620733 Merge pull request 'Fix MacOS DMG Location' (#171) from fyne/cwtch-ui:fix-macos-dmg-location into trunk
Reviewed-on: cwtch.im/cwtch-ui#171
2021-09-09 00:24:09 +00:00
Dan Ballard 652892af4c Merge pull request 'gitignore more libcwtch/tor binaries' (#178) from mal/cwtch-ui:gitignore into trunk
Reviewed-on: cwtch.im/cwtch-ui#178
2021-09-08 23:12:27 +00:00
Sarah Jamie Lewis dc69d34484 Merge pull request 'drone checkout from forked repo' (#179) from dan/cwtch-ui:drone-fork into trunk
Reviewed-on: cwtch.im/cwtch-ui#179
2021-09-08 22:32:09 +00:00
Sarah Jamie Lewis 9abee953a6 Merge branch 'trunk' into drone-fork 2021-09-08 22:31:58 +00:00
fyne 86dd8d7f78 Merge branch 'trunk' into fix-macos-dmg-location 2021-09-08 18:36:13 +00:00
Trevor Bergeron 34793ca459
gitignore more libcwtch/tor binaries 2021-09-08 13:03:25 -04:00
Dan Ballard cafac4aa19 drone checkout from forked repo 2021-09-08 09:56:40 -07:00
Dan Ballard 43b7872438 Merge pull request 'linux: exec cwtch instead of keeping sh process' (#165) from mal/cwtch-ui:exec into trunk
Reviewed-on: cwtch.im/cwtch-ui#165
2021-09-08 16:17:53 +00:00
Sarah Jamie Lewis bc24c7f3c7 Merge pull request 'drone checkout from forked repo' (#177) from drone-fork into trunk
Reviewed-on: cwtch.im/cwtch-ui#177
2021-09-08 01:11:33 +00:00
Dan Ballard 47510be645 drone checkout from forked repo 2021-09-07 17:59:14 -07:00
fyne be0c4f4d64 Leave the Cwtch.app local path in 2021-09-04 18:48:49 +01:00
fyne b53f43d946 Allow the fallback to the Tor Browser tor.real 2021-09-04 14:41:01 +01:00
fyne 13f76d1861 Attempt to fix #166
Add better checks
2021-09-04 14:34:42 +01:00
Trevor Bergeron 10b43a5ff6
linux: exec cwtch instead of keeping sh process 2021-09-01 23:20:50 -04:00
Sarah Jamie Lewis 7737085a28 Merge pull request 'linux-install-fix' (#164) from linux-install-fix into trunk
Reviewed-on: cwtch.im/cwtch-ui#164
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-08-30 23:10:31 +00:00
Dan Ballard f1944dbc9e pubspec version update 2021-08-30 16:01:08 -07:00
Dan Ballard 99762c0b29 update README for mac build instructions to get libcwtch-go; translations update 2021-08-30 15:50:24 -07:00
Dan Ballard 5764e2d725 update linux install scripts and LIBCWTCH-GO version 2021-08-30 15:34:56 -07:00
Sarah Jamie Lewis 476bdb626c Merge pull request 'macos build and packaging' (#160) from macos into trunk
Reviewed-on: cwtch.im/cwtch-ui#160
2021-08-30 21:30:15 +00:00
Dan Ballard e648068c64 copyright 2021-08-30 17:28:09 -04:00
Dan Ballard a615f30eea finish MacOS packaging work, app icons 2021-08-28 20:41:08 -04:00
Dan Ballard 851e391666 macos build support and packaging scripts 2021-08-28 13:53:35 -04:00
Dan Ballard d461bf879c turn off macos sandbox mode; ffi refactor to get library path so isolate polling appbus events always gets new platform updates 2021-08-28 13:53:35 -04:00
Dan Ballard ca83033997 basic macos stuf support: builds but doesn't run, cant find libCwtch.dylib 2021-08-28 13:53:35 -04:00
Sarah Jamie Lewis 7b88415fab Merge pull request 'drag messagelist scrollbar fix' (#159) from scrollfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#159
2021-08-27 21:59:22 +00:00
erinn d6b6069ef1 drag messagelist scrollbar fix 2021-08-27 14:58:12 -07:00
erinn 08635aec7f Merge pull request 'Fix Android Delete Contact API' (#158) from archive into trunk
Reviewed-on: cwtch.im/cwtch-ui#158
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-08-27 21:28:36 +00:00
Sarah Jamie Lewis 98363a11b6 Fix Android Delete Contact API 2021-08-27 14:25:20 -07:00
erinn 7849deda95 Merge pull request 'Distinguish between Archive and Delete' (#157) from archive into trunk
Reviewed-on: cwtch.im/cwtch-ui#157
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-08-27 21:04:25 +00:00
Sarah Jamie Lewis 31b3d48f44 Update API 2021-08-27 14:02:16 -07:00
Sarah Jamie Lewis 4e3bd696ed Distinguish between Archive and Delete 2021-08-27 13:46:50 -07:00
erinn 00e4ed30f0 Merge pull request 'Improve Button Feedback' (#156) from buttonfeedback into trunk
Reviewed-on: cwtch.im/cwtch-ui#156
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-08-27 20:17:22 +00:00
Sarah Jamie Lewis 30ea12a9ce Improve Button Feedback 2021-08-27 11:41:10 -07:00
erinn 397971a181 Merge pull request 'Block Unknown Connections Indicator' (#154) from blockunknown into trunk
Reviewed-on: cwtch.im/cwtch-ui#154
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-08-26 21:58:49 +00:00
Sarah Jamie Lewis 6210a64315 Format 2021-08-26 14:56:58 -07:00
Sarah Jamie Lewis 825fb23992 Block Unknown Connections Indicator 2021-08-26 14:53:48 -07:00
Sarah Jamie Lewis a06815fa08 Merge pull request 'l10n updates' (#153) from l10nupdates into trunk
Reviewed-on: cwtch.im/cwtch-ui#153
2021-08-26 21:51:25 +00:00
erinn 62a9402357 l10n updates 2021-08-26 14:48:14 -07:00
Sarah Jamie Lewis 67ed282394 Merge pull request 'clean up compose bar' (#152) from erinn45 into trunk
Reviewed-on: cwtch.im/cwtch-ui#152
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-08-26 21:22:52 +00:00
erinn e487d0bd65 Merge branch 'trunk' into erinn45 2021-08-26 21:20:05 +00:00
erinn 76c6a5f069 merge trunk 2021-08-26 14:18:25 -07:00
erinn 004e211d0b Merge pull request 'Add Physics to Message Row. Fixes: #132' (#151) from animate into trunk
Reviewed-on: cwtch.im/cwtch-ui#151
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-08-26 21:16:50 +00:00
Sarah Jamie Lewis afe2457062 Merge branch 'trunk' into animate 2021-08-26 21:12:39 +00:00
Dan Ballard 42ed710fc9 Merge pull request 'Bandaid fix - update the Tor tarball to fetch for Windows builds to a more recent version' (#149) from mvaneerde/cwtch-ui:windows-tor-ball into trunk
Reviewed-on: cwtch.im/cwtch-ui#149
2021-08-26 21:12:32 +00:00
Sarah Jamie Lewis 1a80b4b808 Add Physics to Message Row. Fixes: #132 2021-08-26 14:11:10 -07:00
erinn 61d1a60b0e nicer compose bar 2021-08-26 13:52:17 -07:00
Matthew van Eerde (^_^) 7d15c41aed Pull the old version from the Cwtch archive rather than the new version from Tor directly 2021-08-26 10:41:06 -07:00
Matthew van Eerde (^_^) f09c40d7dd Bandaid fix - update the Tor tarball to fetch for Windows builds to a more recent version 2021-08-26 09:51:03 -07:00
erinn b78f138cb2 Merge pull request 'Memory Management Improvements' (#148) from memory into trunk
Reviewed-on: cwtch.im/cwtch-ui#148
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-08-25 19:37:16 +00:00
Sarah Jamie Lewis 6d4ab5bab3 Upgrade libcwtch-go 2021-08-25 09:59:49 -07:00
Sarah Jamie Lewis 3cf81e41d6 Memory Management Improvements
Free Pointers Allocated by NativeUTF8

Also use new c_FreePointer from libCwtch to free returned strings
after they have been processed.
2021-08-24 22:10:59 -07:00
Dan Ballard ebb665355c Merge pull request 'Enable incognito keyboard on android' (#142) from disable-ime-learning into trunk
Reviewed-on: cwtch.im/cwtch-ui#142
2021-08-17 20:57:23 +00:00
Dan Ballard aee861df90 Merge branch 'trunk' into disable-ime-learning 2021-08-17 20:49:02 +00:00
Sarah Jamie Lewis 132f1718ed Merge pull request '.drone.yml: update all flutter images to 2.5, change win tor file to op buildfiles mirror' (#146) from drone-ver into trunk
Reviewed-on: cwtch.im/cwtch-ui#146
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-08-17 20:34:47 +00:00
Dan Ballard 5a8a39c77f .drone.yml: update all flutter images to 2.5, change win tor file to op buildfiles mirror 2021-08-17 13:21:51 -07:00
Sarah Jamie Lewis e4d9829e7b Merge pull request 'unread messages scrollposition: fix dualpane and performance issues' (#145) from erinn48 into trunk
Reviewed-on: cwtch.im/cwtch-ui#145
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-08-16 23:51:07 +00:00
erinn 06075583ed Merge branch 'trunk' into erinn48 2021-08-16 23:40:55 +00:00
erinn 62bedcd612 Merge pull request 'Format Dates to match OS Locale' (#144) from date-format into trunk
Reviewed-on: cwtch.im/cwtch-ui#144
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-08-16 23:40:41 +00:00
erinn b3f9f10cdd Merge branch 'trunk' into erinn48 2021-08-16 23:36:32 +00:00
erinn 65811231a7 anudda performance fix 2021-08-16 16:34:44 -07:00
erinn 0615a81042 unread message scrollposition: dualpane and performance fixes 2021-08-16 16:09:03 -07:00
Sarah Jamie Lewis 587bb783aa Format Dates to match OS Locale 2021-08-16 15:04:05 -07:00
Sarah Jamie Lewis 94396d965b Merge pull request 'new buildfiles host' (#139) from new_buildfiles into trunk
Reviewed-on: cwtch.im/cwtch-ui#139
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-08-16 19:52:40 +00:00
Sarah Jamie Lewis a19deb2b4d Merge branch 'trunk' into disable-ime-learning 2021-08-09 15:37:07 +00:00
Sarah Jamie Lewis 505bceb887 Enable incognito keyboard on android 2021-08-09 08:35:45 -07:00
Dan Ballard 83c9adac5d new buildfiles host 2021-08-06 14:50:58 -07:00
Sarah Jamie Lewis a1e168a5f7 Merge pull request 'opening a chat with unread messages should go to last read. fixes #48' (#133) from erinn48 into trunk
Reviewed-on: cwtch.im/cwtch-ui#133
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-07-23 14:56:30 -07:00
erinn 08fcbc6a10 opening a chat with unread messages should go to last read. fixes #48 2021-07-23 14:50:21 -07:00
Dan Ballard 8668211e1d Merge pull request 'BUGFIX: Unblock Profile' (#128) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#128
2021-07-15 14:59:09 -07:00
Sarah Jamie Lewis 16bdcce25c BUGFIX: Unblock Profile 2021-07-15 14:58:24 -07:00
Sarah Jamie Lewis f9584d1301 Merge pull request '1.1 release finalization' (#125) from r1.1 into trunk
Reviewed-on: cwtch.im/cwtch-ui#125
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-07-15 13:34:18 -07:00
Dan Ballard 13faa2c027 1.1 release finalization 2021-07-15 13:22:15 -07:00
Dan Ballard a7536d30ea Merge pull request 'Android Icons + Debug Fixes' (#124) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#124
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-07-15 13:14:46 -07:00
Sarah Jamie Lewis 0abc4caf95 Android Icons + Debug Fixes 2021-07-15 13:07:11 -07:00
Sarah Jamie Lewis 07ed216b0a Merge pull request 'update icons to chibi bw icon. add script for icon generation' (#123) from iconUpdate into trunk
Reviewed-on: cwtch.im/cwtch-ui#123
2021-07-15 11:24:34 -07:00
Sarah Jamie Lewis f1594be932 Merge branch 'trunk' into iconUpdate 2021-07-15 11:24:27 -07:00
Dan Ballard e7ecd28faf Merge pull request 'Fixup Group Invite Accept Timestamp + Syncing Indicator' (#122) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#122
2021-07-15 11:23:55 -07:00
Dan Ballard 19269fce1c update icons to chibi bw icon. add script for icon generation 2021-07-15 11:14:17 -07:00
Sarah Jamie Lewis 91c556dd14 Fixup Group Invite Accept Timestamp + Syncing Indicator 2021-07-15 10:57:40 -07:00
Dan Ballard 1726ea2f45 Merge pull request 'Fix Last Message Time for NewGroupMessages' (#121) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#121
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-07-15 10:30:07 -07:00
Sarah Jamie Lewis 6723949d17 Fix Last Message Time for NewGroupMessages 2021-07-15 10:03:19 -07:00
Sarah Jamie Lewis 43d0e51890 Merge pull request 'hilight unlock button on 0 profiles' (#120) from hilightUnlock into trunk
Reviewed-on: cwtch.im/cwtch-ui#120
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-07-14 16:30:41 -07:00
Dan Ballard 51d2e89e9a hilight unlock button on 0 profiles 2021-07-14 16:19:11 -07:00
Sarah Jamie Lewis cac05cd4e6 Merge pull request 'lightFixes' (#119) from lightFixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#119
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-07-14 15:30:53 -07:00
Dan Ballard 03fd76c598 add quality.sh and run 2021-07-14 15:07:29 -07:00
Dan Ballard cddf204695 quote message color fixes; disable swipe reply on desktop; message view alert color fix; card theme fix 2021-07-14 15:07:29 -07:00
Sarah Jamie Lewis 464edf9aed Merge pull request 'Autohide messages from blocked contacts' (#117) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#117
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-07-14 13:48:02 -07:00
Sarah Jamie Lewis b9ce0176f9 Merge branch 'trunk' into bugfix 2021-07-14 13:39:18 -07:00
Sarah Jamie Lewis 79abd45a83 Merge pull request 'dont show placeholder when contact offline' (#118) from erinn23 into trunk
Reviewed-on: cwtch.im/cwtch-ui#118
2021-07-14 13:39:10 -07:00
erinn 0126721979 Merge branch 'trunk' into erinn23 2021-07-14 13:38:34 -07:00
erinn 248167c15a dont show placeholder when contact offline 2021-07-14 13:37:47 -07:00
Sarah Jamie Lewis d126cc6ebb Autohide messages from blocked contacts 2021-07-14 13:35:22 -07:00
Sarah Jamie Lewis 7a7b3f22b0 Merge pull request 'enter a message placeholder text' (#116) from erinn23 into trunk
Reviewed-on: cwtch.im/cwtch-ui#116
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-07-14 13:26:14 -07:00
erinn 502abb8eb0 update l10n/pl 2021-07-14 13:13:42 -07:00
erinn 782c838fc8 merge trunk 2021-07-14 13:09:26 -07:00
erinn e9d28e76ea enter a message placeholder text 2021-07-14 13:03:59 -07:00
Sarah Jamie Lewis c63a1ae489 Merge pull request 'Increase borders on White Knott svg' (#115) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#115
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-07-14 12:06:59 -07:00
Dan Ballard c73d96e9ba update linux icon pngs and windows icos from svg white knot update 2021-07-14 12:02:08 -07:00
Sarah Jamie Lewis 681b5aa42b Increase borders on White Knott svg 2021-07-14 11:04:19 -07:00
Dan Ballard 166c1ca726 Merge pull request 'Formatting + Better Align Password Context on Small Screens' (#114) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#114
2021-07-14 10:46:29 -07:00
Sarah Jamie Lewis 6dd2ecb199 Formatting + Better Align Password Context on Small Screens 2021-07-14 10:29:46 -07:00
Dan Ballard 86357be367 Merge pull request 'Use ProfileInfoState instead of ContactListState in MessageRow Test' (#113) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#113
2021-07-14 09:35:01 -07:00
Sarah Jamie Lewis 4628b87c51 Use ProfileInfoState instead of ContactListState in MessageRow Test 2021-07-13 21:09:35 -07:00
Sarah Jamie Lewis 0688b37b26 Merge pull request 'make version text selectable; add profile password warning text; shorten buildver git hash' (#112) from copyVerPassWarn into trunk
Reviewed-on: cwtch.im/cwtch-ui#112
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-07-13 21:02:33 -07:00
Sarah Jamie Lewis e7c05343d6 Merge branch 'trunk' into copyVerPassWarn 2021-07-13 21:02:21 -07:00
Dan Ballard 17e15e5ae6 Merge pull request 'Fix exceptions when quoting with quotes + fix size of message row without toolbar' (#111) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#111
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-07-13 18:38:04 -07:00
Dan Ballard 2dd6886f22 upgrade windows tor to 0.4.6.5 (old version deleted from tor servers) 2021-07-13 18:35:46 -07:00
Dan Ballard 04d8abe4e1 make version text selectable; add profile password warning text; shorten buildver git hash 2021-07-13 18:16:55 -07:00
Sarah Jamie Lewis 70c5325351 Fix exceptions when quoting with quotes + fix size of message row without toolbar 2021-07-13 14:50:27 -07:00
Sarah Jamie Lewis 7b397ecfe2 Merge pull request 'disable message text entry and send button when contact offline' (#107) from noSendOffline into trunk
Reviewed-on: cwtch.im/cwtch-ui#107
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-07-12 15:06:03 -07:00
Sarah Jamie Lewis 3b6638ce8a Merge branch 'trunk' into noSendOffline 2021-07-12 14:52:51 -07:00
Sarah Jamie Lewis 6ea793875f Merge pull request 'model contact uses authorization now; add tooltips to contact pics in chat; dual action: add or goto' (#105) from authTooltip into trunk
Reviewed-on: cwtch.im/cwtch-ui#105
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2021-07-12 14:52:44 -07:00
Sarah Jamie Lewis 80f9ed6a6c Merge branch 'trunk' into authTooltip 2021-07-12 14:44:07 -07:00
Dan Ballard 3fef3153b4 disable message text entry and send button when contact offline 2021-07-12 13:18:39 -07:00
Dan Ballard 3f020472fa model contact uses authorization now; add tooltips to contact pics in chat; dual action: add or goto 2021-07-12 12:01:19 -07:00
Dan Ballard 9bb23de090 Merge pull request 'Max width on UI Landscape Setting' (#104) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#104
2021-07-08 15:52:29 -07:00
Sarah Jamie Lewis 992b26e1ab Max width on UI Landscape Setting
Fix #16
2021-07-08 14:43:35 -07:00
Sarah Jamie Lewis d4ab6585ea UI state fixes
Fix #89
Fix #75
2021-07-08 14:43:35 -07:00
Dan Ballard 1f012b153c Merge pull request 'UI state fixes' (#103) from bugfix into trunk
Reviewed-on: cwtch.im/cwtch-ui#103
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2021-07-08 13:43:31 -07:00
Sarah Jamie Lewis 9029386344 UI state fixes
Fix #89
Fix #75
2021-07-08 13:31:07 -07:00
174 changed files with 9386 additions and 3907 deletions

View File

@ -8,7 +8,7 @@ clone:
steps:
- name: clone
image: cirrusci/flutter:dev
image: cirrusci/flutter:2.8.0
environment:
buildbot_key_b64:
from_secret: buildbot_key_b64
@ -20,17 +20,17 @@ steps:
# force by pass of ssh host key check, less secure
- ssh-keyscan -H git.openprivacy.ca >> ~/.ssh/known_hosts
# use Drone ssh var instead of hardcode to allow forks to build (gogs@git.openprivacy.ca:cwtch.im/cwtch-ui.git)
- git clone $DRONE_GIT_SSH_URL .
- git clone gogs@git.openprivacy.ca:$DRONE_REPO.git .
- git checkout $DRONE_COMMIT
- name: fetch
image: cirrusci/flutter:dev
image: cirrusci/flutter:2.8.0
volumes:
- name: deps
path: /root/.pub-cache
commands:
- ./fetch-tor.sh
- echo `git describe --tags` > VERSION
- echo `git describe --tags --abbrev=1` > VERSION
- echo `date +%G-%m-%d-%H-%M` > BUILDDATE
- flutter pub get
- mkdir deploy
@ -47,7 +47,7 @@ steps:
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
- name: build-linux
image: openpriv/flutter-desktop:linux-dev
image: openpriv/flutter-desktop:linux-fstable-2.8.0
volumes:
- name: deps
path: /root/.pub-cache
@ -61,7 +61,7 @@ steps:
- rm -r cwtch
- name: test-build-android
image: cirrusci/flutter:dev
image: cirrusci/flutter:2.8.0
when:
event: pull_request
volumes:
@ -71,7 +71,7 @@ steps:
- flutter build apk --debug
- name: build-android
image: cirrusci/flutter:dev
image: cirrusci/flutter:2.8.0
when:
event: push
environment:
@ -95,7 +95,7 @@ steps:
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
- name: widget-tests
image: cirrusci/flutter:dev
image: cirrusci/flutter:2.8.0
volumes:
- name: deps
path: /root/.pub-cache
@ -106,6 +106,7 @@ steps:
- name: deploy-buildfiles
image: kroniak/ssh-client
pull: if-not-exists
environment:
BUILDFILES_KEY:
from_secret: buildfiles_key
@ -117,7 +118,7 @@ steps:
- echo $BUILDFILES_KEY > ~/id_rsab64
- base64 -d ~/id_rsab64 > ~/id_rsa
- chmod 400 ~/id_rsa
- export DIR=flwtch-`cat VERSION`-`cat BUILDDATE`
- export DIR=flwtch-`cat BUILDDATE`-`cat VERSION`
- mv deploy $DIR
- cp -r coverage/html $DIR/coverage-tests
- cp -r test/failures $DIR/test-failures || true
@ -125,11 +126,11 @@ steps:
- find . -type f -exec sha256sum {} \; > ./../sha256s.txt
- mv ./../sha256s.txt .
- cd ..
# TODO: do deployment once files actaully compile
- scp -r -o StrictHostKeyChecking=no -i ~/id_rsa $DIR buildfiles@openprivacy.ca:/home/buildfiles/buildfiles/
- scp -r -o StrictHostKeyChecking=no -i ~/id_rsa $DIR buildfiles@build.openprivacy.ca:/home/buildfiles/buildfiles/
- name: notify-email
image: drillster/drone-email
pull: if-not-exists
settings:
host: build.openprivacy.ca
port: 25
@ -140,6 +141,7 @@ steps:
- name: notify-gogs
image: openpriv/drone-gogs
pull: if-not-exists
when:
event: pull_request
status: [ success, changed, failure ]
@ -154,7 +156,7 @@ volumes:
temp: {}
trigger:
repo: cwtch.im/cwtch-ui
#repo: cwtch.im/cwtch-ui # allow forks to build?
branch: trunk
event:
- push
@ -175,7 +177,7 @@ clone:
steps:
- name: clone
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
environment:
buildbot_key_b64:
from_secret: buildbot_key_b64
@ -187,22 +189,22 @@ steps:
- git init
# -o UserKnownHostsFile=../known_hosts
- git config core.sshCommand 'ssh -o StrictHostKeyChecking=no -i ../id_rsa'
- git remote add origin $Env:DRONE_GIT_SSH_URL
- git remote add origin gogs@git.openprivacy.ca:$Env:DRONE_REPO.git
- git pull origin trunk
- git fetch --tags
- git checkout $Env:DRONE_COMMIT
- name: fetch
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
commands:
- powershell -command "Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.0.18/tor-win64-0.4.5.9.zip -OutFile tor.zip"
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '72764eb07ad8ab511603aba0734951ca003989f5f4686af91ba220217b4a8a4bcc5f571b59f52c847932f6efedf847b111621983050fcddbb8099d43ca66fb07' ) { Write-Error 'tor.zip sha512sum mismatch' }"
- git describe --tags > VERSION
- powershell -command "Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip"
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }"
- git describe --tags --abbrev=1 > VERSION
- powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE
- .\fetch-libcwtch-go.ps1
- name: build-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
commands:
- flutter pub get
- $Env:version += type .\VERSION
@ -213,15 +215,16 @@ steps:
# flutter hasn't worked out it's packaging of required dll's so we have to resort to this manual nonsense
# https://github.com/google/flutter-desktop-embedding/issues/587
# https://github.com/flutter/flutter/issues/53167
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140.dll $Env:releasedir
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140_1.dll $Env:releasedir
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\msvcp140.dll $Env:releasedir
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30133\x64\Microsoft.VC142.CRT\vcruntime140.dll $Env:releasedir
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30133\x64\Microsoft.VC142.CRT\vcruntime140_1.dll $Env:releasedir
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30133\x64\Microsoft.VC142.CRT\msvcp140.dll $Env:releasedir
- copy README.md $Env:releasedir\
- copy windows\*.bat $Env:releasedir\
- powershell -command "Expand-Archive -Path tor.zip -DestinationPath $Env:releasedir\Tor"
- name: package-windows
image: openpriv/nsis:latest
image: openpriv/nsis
pull: if-not-exists
when:
event: push
status: [ success ]
@ -238,7 +241,7 @@ steps:
- $Env:zipsha = $Env:zip + '.sha512'
- $Env:msix = 'cwtch-install-' + $Env:version + '.msix'
- $Env:msixsha = $Env:msix + '.sha512'
- $Env:buildname = 'flwtch-win-' + $Env:version + '-' + $Env:builddate
- $Env:buildname = 'flwtch-win-' + $Env:builddate + '-' + $Env:version
- $Env:builddir = $Env:buildname
- echo $Env:pfx > codesign.pfx.b64
- certutil -decode codesign.pfx.b64 codesign.pfx
@ -258,7 +261,7 @@ steps:
- move *.sha512 deploy\$Env:builddir
- name: deploy-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
when:
event: push
status: [ success ]
@ -268,11 +271,94 @@ steps:
commands:
- echo $Env:BUILDFILES_KEY > id_rsab64
- certutil -decode id_rsab64 id_rsa
- scp -r -o StrictHostKeyChecking=no -i id_rsa deploy\\* buildfiles@openprivacy.ca:/home/buildfiles/buildfiles/
- scp -r -o StrictHostKeyChecking=no -i id_rsa deploy\\* buildfiles@build.openprivacy.ca:/home/buildfiles/buildfiles/
trigger:
repo: cwtch.im/cwtch-ui
# repo: cwtch.im/cwtch-ui # allow forks to build?
branch: trunk
event:
- push
- pull_request
---
kind: pipeline
type: exec
name: macos
platform:
os: darwin
arch: amd64
clone:
disable: true
steps:
- name: clone
environment:
buildbot_key_b64:
from_secret: buildbot_key_b64
commands:
- mkdir ~/.ssh
- echo $buildbot_key_b64 > ~/.ssh/id_rsa.b64
- base64 -d ~/.ssh/id_rsa.b64 > ~/.ssh/id_rsa
- chmod 400 ~/.ssh/id_rsa
# force by pass of ssh host key check, less secure
- ssh-keyscan -H git.openprivacy.ca >> ~/.ssh/known_hosts
- git init
- git config core.sshCommand 'ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa'
- git remote add origin gogs@git.openprivacy.ca:$DRONE_REPO.git
- git pull origin trunk
- git fetch --tags
- git checkout $DRONE_COMMIT
# use Drone ssh var instead of hardcode to allow forks to build (gogs@git.openprivacy.ca:cwtch.im/cwtch-ui.git)
#- git clone gogs@git.openprivacy.ca:$DRONE_REPO.git .
#- git checkout $DRONE_COMMIT
- name: fetch
commands:
- ./fetch-tor-macos.sh
- echo `git describe --tags --abbrev=1` > VERSION
- echo `date +%G-%m-%d-%H-%M` > BUILDDATE
- export PATH=$PATH:/Users/Dan/development/flutter/bin
- flutter pub get
- mkdir deploy
- ./fetch-libcwtch-go-macos.sh
- gem install --user-install cocoapods
- name: build-macos
commands:
- export PATH=$PATH:/Users/Dan/development/flutter/bin
- export GEM_HOME=$HOME/.gem
- export PATH=$GEM_HOME/ruby/2.6.0/bin:$PATH
- flutter config --enable-macos-desktop
- flutter build macos --dart-define BUILD_VER=`cat VERSION` --dart-define BUILD_DATE=`cat BUILDDATE`
- export PATH=$PATH:/usr/local/bin #create-dmg
- macos/package-release.sh
- mkdir -p deploy
- mv Cwtch.dmg deploy
- name: deploy-buildfiles
environment:
BUILDFILES_KEY:
from_secret: buildfiles_key
when:
event: push
status: [ success ]
commands:
- echo $BUILDFILES_KEY > ~/id_rsab64
- base64 -d ~/id_rsab64 > ~/id_rsa
- chmod 400 ~/id_rsa
- export DIR=flwtch-macos-`cat BUILDDATE`-`cat VERSION`
- mv deploy $DIR
- cd $DIR
- find . -type f -exec shasum -a 512 {} \; > ./../sha512s.txt
- mv ./../sha512s.txt .
- cd ..
- scp -r -o StrictHostKeyChecking=no -i ~/id_rsa $DIR buildfiles@build.openprivacy.ca:/home/buildfiles/buildfiles/
trigger:
#repo: cwtch.im/cwtch-ui # allow forks to build?
branch: trunk
event:
- push
- pull_request

6
.gitignore vendored
View File

@ -40,8 +40,10 @@ app.*.symbols
# Obfuscation related
app.*.map.json
libCwtch.so
linux/tor
linux/libCwtch.so
android/cwtch/cwtch.aar
android/app/src/main/jniLibs/*/libtor.so
coverage
test/failures
.gradle
.gradle

View File

@ -0,0 +1 @@
2021-12-20-18-47-v1.5.2

View File

@ -1 +1 @@
v1.0.0-27-g4d218df-2021-07-07-23-27
2021-12-20-23-47-v1.5.2

View File

@ -12,40 +12,65 @@ This README covers build instructions, for information on Cwtch itself please go
- `install.home.sh` installs the app into your home directory
- `install.sys.sh` as root to install system wide
- or run out of the unziped directory
- MacOS: Available from [https://cwtch.im/download/](https://cwtch.im/download/) as a .dmg
## Running
Cwtch processes the following environment variables:
- `CWTCH_HOME=` overrides the default storage path of `~/.cwtch` with what ever you choose
- `LOG_FILE=` will reroute all of libcwtch-go's logging to the specified file instead of the console
- `LOG_FILE=` will reroute all of libcwtch-go's logging to the specified file instead of the console
- `LOG_LEVEL=debug` will set the log level to debug instead of info
## Building
### Getting Started
First you will need a valid [flutter sdk installation](https://flutter.dev/docs/get-started/install)
and run `flutter pub get` to fetch dependencies.
First you will need a valid [flutter sdk installation](https://flutter.dev/docs/get-started/install).
You will probably want to disable Analytics on the Flutter Tool: `flutter config --no-analytics`
This project uses the flutter `stable` channel
Once flutter is set up, run `flutter pub get` from this project folder to fetch dependencies.
By default a development version is built, which loads profiles from `$CWTCH_HOME/dev/`. This is so that you can build
and test development builds with alternative profiles while running a release/stable version of Cwtch uninterrupted.
To build a release version and load normal profiles, use `build-release.sh X` instead of `flutter build X`
### Building on Linux (for Linux)
- run `fetch-libcwtch-go.sh`libCwtch-go to fetch a prebuild version of `libCwtch-go.so` go to `./linux`. Include `./linux` in `LD_LIBRARY_PATH`
- run `fetch-tor.sh` and/or ensure that `tor` is in `$PATH`
- run `flutter run -d linux`
- copy `libCwtch-go.so` to `linux/`, or run `fetch-libcwtch-go.sh` to download it
- set `LD_LIBRARY_PATH="$PWD/linux"`
- copy a `tor` binary to `linux/` or run `fetch-tor.sh` to download one
- run `flutter config --enable-linux-desktop` if you've never done so before
- optional: launch cwtch-ui debug build by running `flutter run -d linux`
- to build cwtch-ui, run `flutter build linux`
- optional: launch cwtch-ui release build with `env LD_LIBRARY_PATH=linux ./build/linux/x64/release/bundle/cwtch`
- to package the build, run `linux/package-release.sh`
### Building on Windows (for Windows)
- run `fetch-libcwtch-go.ps1` to fetch a prebuild version of `libCwtch.dll`
- copy `libCwtch.dll` to `windows/`, or run `fetch-libcwtch-go.ps1` to download it
- run `fetch-tor-win.ps1` to fetch Tor for windows
- run `flutter run -d windows`
- optional: launch cwtch-ui debug build by running `flutter run -d windows`
- to build cwtch-ui, run `flutter build windows`
- optional: to run the release build:
- `cp windows/libCwtch.dll .`
- `./build/windows/runner/Release/cwtch.exe`
### Building on Linux/Windows (for Android)
- Follow the steps above to fetch `libCwtch-go` and `tor` (these will fetch Android versions of these binaries also)
- run `flutter run` with an Android phone connect via USB (or some other valid debug mode)
### Building on MacOS
- Cocaopods is required, you may need to `gem install cocaopods -v 1.9.3`
- copy `libCwtch.dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it
- run `fetch-tor-macos.sh` to fetch Tor or Download and install Tor Browser and `cp -r /Applications/Tor\ Browser.app/Contents/MacOS/Tor ./macos/`
- `flutter build macos`
- optional: launch cwtch-ui release build with `./build/macos/Build/Products/Release/Cwtch.app/Contents/MacOS/Cwtch`
- To package the UI: `./macos/package-release.sh`, which results in a Cwtch.dmg that has libCwtch.dylib and tor in it as well and can be installed into Applications
### Known Platform Issues
- **Windows**: Flutter engine has a [known bug](https://github.com/flutter/flutter/issues/75675) around the Right Shift key being sticky.
@ -68,6 +93,7 @@ In Lokalise, hit Download and make sure:
* Format is set to "Flutter (.arb)
* Output filename is set to `l10n/intl_%LANG_ISO%.%FORMAT%`
* Empty translations is set to "Replace with base language"
* Order "Last Update"
Build, download and unzip the output, overwriting `lib/l10n`. The next time Flwtch is built, Flutter will notice the changes and update `app_localizations.dart` accordingly (thanks to `generate:true` in `pubspec.yaml`).

View File

@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 29
compileSdkVersion 30
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -48,7 +48,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "im.cwtch.flwtch"
minSdkVersion 16
targetSdkVersion 29
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View File

@ -48,4 +48,11 @@
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</manifest>

View File

@ -15,6 +15,10 @@ import cwtch.Cwtch
import io.flutter.FlutterInjector
import org.json.JSONObject
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import android.net.Uri
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
@ -56,6 +60,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
val downloadIDs = mutableMapOf<String, Int>()
while(true) {
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
@ -87,12 +92,71 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
.setContentTitle(data.getString("Nick"))
.setContentText("New message")//todo: translate
.setLargeIcon(BitmapFactory.decodeStream(fh))
.setSmallIcon(R.mipmap.knott)
.setSmallIcon(R.mipmap.knott_transparent)
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.build()
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), handle), newNotification)
}
} else if (evt.EventType == "FileDownloadProgressUpdate") {
try {
val data = JSONObject(evt.Data);
val fileKey = data.getString("FileKey");
val title = data.getString("NameSuggestion");
val progress = data.getString("Progress").toInt();
val progressMax = data.getString("FileSizeInChunks").toInt();
if (!downloadIDs.containsKey(fileKey)) {
downloadIDs.put(fileKey, downloadIDs.count());
}
var dlID = downloadIDs.get(fileKey);
if (dlID == null) {
dlID = 0;
}
if (progress >= 0) {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createDownloadNotificationChannel(fileKey, fileKey)
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
};
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setOngoing(true)
.setContentTitle("Downloading")//todo: translate
.setContentText(title)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setProgress(progressMax, progress, false)
.setSound(null)
//.setSilent(true)
.build();
notificationManager.notify(dlID, newNotification);
}
} catch (e: Exception) {
Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
}
} else if (evt.EventType == "FileDownloaded") {
Log.d("FlwtchWorker", "file downloaded!");
val data = JSONObject(evt.Data);
val tempFile = data.getString("TempFile");
val fileKey = data.getString("FileKey");
if (tempFile != "" && tempFile != data.getString("FilePath")) {
val filePath = data.getString("FilePath");
Log.i("FlwtchWorker", "moving "+tempFile+" to "+filePath);
val sourcePath = Paths.get(tempFile);
val targetUri = Uri.parse(filePath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os);
Log.d("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) {
os?.flush();
os?.close();
Files.delete(sourcePath);
}
}
if (downloadIDs.containsKey(fileKey)) {
notificationManager.cancel(downloadIDs.get(fileKey)?:0);
}
}
Intent().also { intent ->
@ -116,46 +180,90 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val pass = (a.get("pass") as? String) ?: ""
Cwtch.loadProfiles(pass)
}
"ChangePassword" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val pass = (a.get("OldPass") as? String) ?: ""
val passNew = (a.get("NewPass") as? String) ?: ""
val passNew2 = (a.get("NewPassAgain") as? String) ?: ""
Cwtch.changePassword(profile, pass, passNew, passNew2)
}
"GetMessage" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val indexI = a.getInt("index")
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build())
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val indexI = a.getInt("index").toLong()
Log.d("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString())
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build())
}
"GetMessageByID" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val id = a.getInt("id").toLong()
return Result.success(Data.Builder().putString("result", Cwtch.getMessageByID(profile, conversation, id)).build())
}
"GetMessageByContentHash" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val contentHash = (a.get("contentHash") as? String) ?: ""
return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, handle, contentHash)).build())
}
"UpdateMessageFlags" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val midx = (a.get("midx") as? Long) ?: 0
val flags = (a.get("flags") as? Long) ?: 0
Cwtch.updateMessageFlags(profile, handle, midx, flags)
}
"AcceptContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
Cwtch.acceptContact(profile, handle)
val conversation = a.getInt("conversation").toLong()
val contentHash = (a.get("contentHash") as? String) ?: ""
return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, conversation, contentHash)).build())
}
"UpdateMessageAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val channel = a.getInt("chanenl").toLong()
val midx = a.getInt("midx").toLong()
val key = (a.get("key") as? String) ?: ""
val value = (a.get("value") as? String) ?: ""
Cwtch.setMessageAttribute(profile, conversation, channel, midx, key, value)
}
"AcceptConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
Cwtch.acceptConversation(profile, conversation)
}
"BlockContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
Cwtch.blockContact(profile, handle)
val conversation = a.getInt("conversation").toLong()
Cwtch.blockContact(profile, conversation)
}
"SendMessage" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val message = (a.get("message") as? String) ?: ""
Cwtch.sendMessage(profile, handle, message)
Cwtch.sendMessage(profile, conversation, message)
}
"SendInvitation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val target = (a.get("target") as? String) ?: ""
Cwtch.sendInvitation(profile, handle, target)
val conversation = a.getInt("conversation").toLong()
val target = (a.get("target") as? Long) ?: -1
Cwtch.sendInvitation(profile, conversation, target)
}
"ShareFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: ""
Cwtch.shareFile(profile, conversation, filepath)
}
"DownloadFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: ""
val manifestpath = (a.get("manifestpath") as? String) ?: ""
val filekey = (a.get("filekey") as? String) ?: ""
// FIXME: Prevent spurious calls by Intent
if (profile != "") {
Cwtch.downloadFile(profile, conversation, filepath, manifestpath, filekey)
}
}
"CheckDownloadStatus" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val fileKey = (a.get("fileKey") as? String) ?: ""
Cwtch.checkDownloadStatus(profile, fileKey)
}
"VerifyOrResumeDownload" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val fileKey = (a.get("fileKey") as? String) ?: ""
Cwtch.verifyOrResumeDownload(profile, conversation, fileKey)
}
"SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: ""
@ -174,13 +282,6 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val bundle = (a.get("bundle") as? String) ?: ""
Cwtch.importBundle(profile, bundle)
}
"SetGroupAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
val key = (a.get("key") as? String) ?: ""
val value = (a.get("value") as? String) ?: ""
Cwtch.setGroupAttribute(profile, groupHandle, key, value)
}
"CreateGroup" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val server = (a.get("server") as? String) ?: ""
@ -192,26 +293,75 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val pass = (a.get("pass") as? String) ?: ""
Cwtch.deleteProfile(profile, pass)
}
"LeaveConversation" -> {
"ArchiveConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val contactHandle = (a.get("contactHandle") as? String) ?: ""
Cwtch.leaveConversation(profile, contactHandle)
val conversation = (a.get("conversation") as? Long) ?: -1
Cwtch.archiveConversation(profile, conversation)
}
"LeaveGroup" -> {
"DeleteConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.leaveGroup(profile, groupHandle)
val conversation = (a.get("conversation") as? Long) ?: -1
Cwtch.deleteContact(profile, conversation)
}
"RejectInvite" -> {
"SetProfileAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.rejectInvite(profile, groupHandle)
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setProfileAttribute(profile, key, v)
}
"SetConversationAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = (a.get("conversation") as? Long) ?: -1
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setConversationAttribute(profile, conversation, key, v)
}
"Shutdown" -> {
Cwtch.shutdownCwtch();
return Result.success()
}
else -> return Result.failure()
"LoadServers" -> {
val password = (a.get("Password") as? String) ?: ""
Cwtch.loadServers(password)
}
"CreateServer" -> {
val password = (a.get("Password") as? String) ?: ""
val desc = (a.get("Description") as? String) ?: ""
val autostart = (a.get("Autostart") as? Boolean) ?: false
Cwtch.createServer(password, desc, autostart)
}
"DeleteServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
val password = (a.get("Password") as? String) ?: ""
Cwtch.deleteServer(serverOnion, password)
}
"LaunchServers" -> {
Cwtch.launchServers()
}
"LaunchServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
Cwtch.launchServer(serverOnion)
}
"StopServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
Cwtch.stopServer(serverOnion)
}
"StopServers" -> {
Cwtch.stopServers()
}
"DestroyServers" -> {
Cwtch.destroyServers()
}
"SetServerAttribute" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setServerAttribute(serverOnion, key, v)
}
else -> {
Log.i("FlwtchWorker", "unknown command: " + method);
return Result.failure()
}
}
return Result.success()
}
@ -240,7 +390,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
.setContentTitle(title)
.setTicker(title)
.setContentText(progress)
.setSmallIcon(R.mipmap.knott)
.setSmallIcon(R.mipmap.knott_transparent)
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
@ -268,6 +418,15 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
return channelId
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createDownloadNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
chan.lightColor = Color.MAGENTA
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
notificationManager.createNotificationChannel(chan)
return channelId
}
companion object {
const val KEY_METHOD = "KEY_METHOD"
const val KEY_ARGS = "KEY_ARGS"

View File

@ -12,16 +12,28 @@ import android.view.Window
import androidx.lifecycle.Observer
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.*
import io.flutter.embedding.android.SplashScreen
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.ErrorLogResult
import org.json.JSONObject
import java.util.concurrent.TimeUnit
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import android.net.Uri
import android.provider.DocumentsContract
import android.content.ContentUris
import android.os.Build
import android.os.Environment
import android.database.Cursor
import android.provider.MediaStore
class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView()
@ -47,6 +59,15 @@ class MainActivity: FlutterActivity() {
private var notificationClickChannel: MethodChannel? = null
private var shutdownClickChannel: MethodChannel? = null
// "Download to..." prompt extra arguments
private val FILEPICKER_REQUEST_CODE = 234
private val PREVIEW_EXPORT_REQUEST_CODE = 235
private var dlToProfile = ""
private var dlToHandle = ""
private var dlToFileKey = ""
private var exportFromPath = ""
// handles clicks received from outside the app (ie, notifications)
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (notificationClickChannel == null || intent.extras == null) return
@ -68,6 +89,42 @@ class MainActivity: FlutterActivity() {
}
}
// handles return values from the system file picker
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
super.onActivityResult(requestCode, result, intent);
if (intent == null || intent!!.getData() == null) {
Log.i("MainActivity:onActivityResult", "user canceled activity");
return;
}
if (requestCode == FILEPICKER_REQUEST_CODE) {
val filePath = intent!!.getData().toString();
val manifestPath = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.dlToFileKey).toString();
handleCwtch(MethodCall("DownloadFile", mapOf(
"ProfileOnion" to this.dlToProfile,
"handle" to this.dlToHandle,
"filepath" to filePath,
"manifestpath" to manifestPath,
"filekey" to this.dlToFileKey
)), ErrorLogResult(""));//placeholder; this Result is never actually invoked
} else if (requestCode == PREVIEW_EXPORT_REQUEST_CODE) {
val targetPath = intent!!.getData().toString()
var srcFile = File(this.exportFromPath)
Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file")
val sourcePath = Paths.get(this.exportFromPath);
val targetUri = Uri.parse(targetPath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os);
Log.d("MainActivity:PREVIEW_EXPORT", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) {
os?.flush();
os?.close();
//Files.delete(sourcePath);
}
}
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Note: this methods are invoked on the main thread.
@ -110,11 +167,7 @@ class MainActivity: FlutterActivity() {
// that we can divert this method call to ReconnectCwtchForeground instead if so.
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
for (workInfo in works) {
Log.i("handleCwtch:WorkManager", "$workInfo")
if (!workInfo.tags.contains(uniqueTag)) {
Log.i("handleCwtch:WorkManager", "canceling ${workInfo.id} bc tags don't include $uniqueTag")
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
}
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
}
WorkManager.getInstance(this).pruneWork()
@ -125,6 +178,38 @@ class MainActivity: FlutterActivity() {
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
return
} else if (call.method == "CreateDownloadableFile") {
this.dlToProfile = argmap["ProfileOnion"] ?: ""
this.dlToHandle = argmap["handle"] ?: ""
val suggestedName = argmap["filename"] ?: "filename.ext"
this.dlToFileKey = argmap["filekey"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/octet-stream"
putExtra(Intent.EXTRA_TITLE, suggestedName)
}
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
return
} else if (call.method == "ExportPreviewedFile") {
this.exportFromPath = argmap["Path"] ?: ""
val suggestion = argmap["FileName"] ?: "filename.ext"
var imgType = "jpeg"
if (suggestion.endsWith("png")) {
imgType = "png"
} else if (suggestion.endsWith("webp")) {
imgType = "webp"
} else if (suggestion.endsWith("bmp")) {
imgType = "bmp"
} else if (suggestion.endsWith("gif")) {
imgType = "gif"
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/" + imgType
putExtra(Intent.EXTRA_TITLE, suggestion)
}
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
return
}
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
@ -178,20 +263,6 @@ class MainActivity: FlutterActivity() {
WorkManager.getInstance(this).pruneWork()
}
// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095
// for reference:
//
// class Response(json: String) : JSONObject(json) {
// val type: String? = this.optString("type")
// val data = this.optJSONArray("data")
// ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } // returns an array of JSONObject
// ?.map { Foo(it.toString()) } // transforms each JSONObject of the array into Foo
// }
//
// class Foo(json: String) : JSONObject(json) {
// val id = this.optInt("id")
// val title: String? = this.optString("title")
// }
class AppbusEvent(json: String) : JSONObject(json) {
val EventType = this.optString("EventType")
val EventID = this.optString("EventID")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -6,7 +6,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:3.5.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}

View File

@ -1,399 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;"
xml:space="preserve"
sodipodi:docname="Cwtch_knott_white.svg"
inkscape:export-filename="/home/sarah/PARA/projects/cwtch/assets/core/knott-white.png"
inkscape:export-xdpi="98.300003"
inkscape:export-ydpi="98.300003"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata35"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs33" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview31"
showgrid="false"
inkscape:zoom="1.3350176"
inkscape:cx="-56.414859"
inkscape:cy="254.41396"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style2">
.st0{fill:#FFFFFF;}
.st1{fill:#010101;}
.st2{fill:#AF9CBA;}
.st3{clip-path:url(#SVGID_2_);}
.st4{clip-path:url(#SVGID_3_);}
.st5{clip-path:url(#SVGID_4_);}
.st6{clip-path:url(#SVGID_6_);}
.st7{clip-path:url(#SVGID_7_);}
.st8{clip-path:url(#SVGID_8_);}
.st9{clip-path:url(#SVGID_10_);}
.st10{clip-path:url(#SVGID_11_);}
.st11{clip-path:url(#SVGID_12_);}
.st12{clip-path:url(#SVGID_14_);}
.st13{clip-path:url(#SVGID_15_);}
.st14{clip-path:url(#SVGID_16_);}
.st15{clip-path:url(#SVGID_18_);}
.st16{clip-path:url(#SVGID_19_);}
.st17{clip-path:url(#SVGID_20_);}
.st18{clip-path:url(#SVGID_22_);}
.st19{clip-path:url(#SVGID_23_);}
.st20{clip-path:url(#SVGID_24_);}
.st21{clip-path:url(#SVGID_26_);}
.st22{clip-path:url(#SVGID_27_);}
.st23{clip-path:url(#SVGID_28_);}
.st24{clip-path:url(#SVGID_30_);}
.st25{clip-path:url(#SVGID_31_);}
.st26{clip-path:url(#SVGID_32_);}
.st27{clip-path:url(#SVGID_34_);}
.st28{clip-path:url(#SVGID_35_);}
.st29{clip-path:url(#SVGID_36_);}
.st30{clip-path:url(#SVGID_38_);}
.st31{clip-path:url(#SVGID_39_);}
.st32{clip-path:url(#SVGID_40_);}
.st33{clip-path:url(#SVGID_42_);}
.st34{clip-path:url(#SVGID_43_);}
.st35{clip-path:url(#SVGID_44_);}
.st36{clip-path:url(#SVGID_46_);}
.st37{clip-path:url(#SVGID_47_);}
.st38{clip-path:url(#SVGID_48_);}
.st39{clip-path:url(#SVGID_50_);}
.st40{clip-path:url(#SVGID_51_);}
.st41{clip-path:url(#SVGID_52_);}
.st42{clip-path:url(#SVGID_54_);}
.st43{clip-path:url(#SVGID_55_);}
.st44{clip-path:url(#SVGID_56_);}
.st45{clip-path:url(#SVGID_58_);}
.st46{clip-path:url(#SVGID_59_);}
.st47{clip-path:url(#SVGID_60_);}
.st48{clip-path:url(#SVGID_62_);}
.st49{clip-path:url(#SVGID_63_);}
.st50{clip-path:url(#SVGID_64_);}
.st51{clip-path:url(#SVGID_66_);}
.st52{clip-path:url(#SVGID_67_);}
.st53{clip-path:url(#SVGID_68_);}
.st54{clip-path:url(#SVGID_70_);}
.st55{clip-path:url(#SVGID_71_);}
.st56{clip-path:url(#SVGID_72_);}
.st57{clip-path:url(#SVGID_74_);}
.st58{clip-path:url(#SVGID_75_);}
.st59{clip-path:url(#SVGID_76_);}
.st60{clip-path:url(#SVGID_78_);}
.st61{clip-path:url(#SVGID_79_);}
.st62{clip-path:url(#SVGID_80_);}
.st63{clip-path:url(#SVGID_82_);}
.st64{clip-path:url(#SVGID_83_);}
.st65{clip-path:url(#SVGID_84_);}
.st66{clip-path:url(#SVGID_86_);}
.st67{clip-path:url(#SVGID_87_);}
.st68{clip-path:url(#SVGID_88_);}
.st69{clip-path:url(#SVGID_90_);}
.st70{clip-path:url(#SVGID_91_);}
.st71{clip-path:url(#SVGID_92_);}
.st72{clip-path:url(#SVGID_94_);}
.st73{clip-path:url(#SVGID_95_);}
.st74{clip-path:url(#SVGID_96_);}
.st75{clip-path:url(#SVGID_98_);}
.st76{clip-path:url(#SVGID_99_);}
.st77{clip-path:url(#SVGID_100_);}
.st78{clip-path:url(#SVGID_102_);}
.st79{clip-path:url(#SVGID_103_);}
.st80{clip-path:url(#SVGID_104_);}
.st81{clip-path:url(#SVGID_106_);}
.st82{clip-path:url(#SVGID_107_);}
.st83{clip-path:url(#SVGID_108_);}
.st84{clip-path:url(#SVGID_110_);}
.st85{clip-path:url(#SVGID_111_);}
.st86{clip-path:url(#SVGID_112_);}
.st87{clip-path:url(#SVGID_114_);}
.st88{clip-path:url(#SVGID_115_);}
.st89{clip-path:url(#SVGID_116_);}
.st90{clip-path:url(#SVGID_118_);}
.st91{clip-path:url(#SVGID_119_);}
.st92{clip-path:url(#SVGID_120_);}
.st93{clip-path:url(#SVGID_122_);}
.st94{clip-path:url(#SVGID_123_);}
.st95{clip-path:url(#SVGID_124_);}
.st96{clip-path:url(#SVGID_126_);}
.st97{clip-path:url(#SVGID_127_);}
.st98{clip-path:url(#SVGID_128_);}
.st99{fill:#64317C;}
.st100{clip-path:url(#SVGID_130_);}
.st101{clip-path:url(#SVGID_131_);}
.st102{clip-path:url(#SVGID_132_);}
.st103{clip-path:url(#SVGID_134_);}
.st104{clip-path:url(#SVGID_135_);}
.st105{clip-path:url(#SVGID_136_);}
.st106{clip-path:url(#SVGID_138_);}
.st107{clip-path:url(#SVGID_139_);}
.st108{clip-path:url(#SVGID_140_);}
.st109{clip-path:url(#SVGID_142_);}
.st110{clip-path:url(#SVGID_143_);}
.st111{clip-path:url(#SVGID_144_);}
.st112{clip-path:url(#SVGID_146_);}
.st113{clip-path:url(#SVGID_147_);}
.st114{clip-path:url(#SVGID_148_);}
.st115{clip-path:url(#SVGID_150_);}
.st116{clip-path:url(#SVGID_151_);}
.st117{clip-path:url(#SVGID_152_);}
.st118{clip-path:url(#SVGID_154_);}
.st119{clip-path:url(#SVGID_155_);}
.st120{clip-path:url(#SVGID_156_);}
.st121{clip-path:url(#SVGID_158_);}
.st122{clip-path:url(#SVGID_159_);}
.st123{clip-path:url(#SVGID_160_);}
.st124{clip-path:url(#SVGID_162_);}
.st125{clip-path:url(#SVGID_163_);}
.st126{clip-path:url(#SVGID_164_);}
.st127{clip-path:url(#SVGID_166_);}
.st128{clip-path:url(#SVGID_167_);}
.st129{clip-path:url(#SVGID_168_);}
.st130{clip-path:url(#SVGID_170_);}
.st131{clip-path:url(#SVGID_171_);}
.st132{clip-path:url(#SVGID_172_);}
.st133{clip-path:url(#SVGID_174_);}
.st134{clip-path:url(#SVGID_175_);}
.st135{clip-path:url(#SVGID_176_);}
.st136{clip-path:url(#SVGID_178_);}
.st137{clip-path:url(#SVGID_179_);}
.st138{clip-path:url(#SVGID_180_);}
.st139{clip-path:url(#SVGID_182_);}
.st140{clip-path:url(#SVGID_183_);}
.st141{clip-path:url(#SVGID_184_);}
.st142{clip-path:url(#SVGID_186_);}
.st143{clip-path:url(#SVGID_187_);}
.st144{clip-path:url(#SVGID_188_);}
.st145{clip-path:url(#SVGID_190_);}
.st146{clip-path:url(#SVGID_191_);}
.st147{clip-path:url(#SVGID_192_);}
.st148{clip-path:url(#SVGID_194_);}
.st149{clip-path:url(#SVGID_195_);}
.st150{clip-path:url(#SVGID_196_);}
.st151{clip-path:url(#SVGID_198_);}
.st152{clip-path:url(#SVGID_199_);}
.st153{clip-path:url(#SVGID_200_);}
.st154{clip-path:url(#SVGID_202_);}
.st155{clip-path:url(#SVGID_203_);}
.st156{clip-path:url(#SVGID_204_);}
.st157{clip-path:url(#SVGID_206_);}
.st158{clip-path:url(#SVGID_207_);}
.st159{clip-path:url(#SVGID_208_);}
.st160{clip-path:url(#SVGID_210_);}
.st161{clip-path:url(#SVGID_211_);}
.st162{clip-path:url(#SVGID_212_);}
.st163{clip-path:url(#SVGID_214_);}
.st164{clip-path:url(#SVGID_215_);}
.st165{clip-path:url(#SVGID_216_);}
.st166{clip-path:url(#SVGID_218_);}
.st167{clip-path:url(#SVGID_219_);}
.st168{clip-path:url(#SVGID_220_);}
.st169{clip-path:url(#SVGID_222_);}
.st170{clip-path:url(#SVGID_223_);}
.st171{clip-path:url(#SVGID_224_);}
.st172{clip-path:url(#SVGID_226_);}
.st173{clip-path:url(#SVGID_227_);}
.st174{clip-path:url(#SVGID_228_);}
.st175{clip-path:url(#SVGID_230_);}
.st176{clip-path:url(#SVGID_231_);}
.st177{clip-path:url(#SVGID_232_);}
.st178{clip-path:url(#SVGID_234_);}
.st179{clip-path:url(#SVGID_235_);}
.st180{clip-path:url(#SVGID_236_);}
.st181{clip-path:url(#SVGID_238_);}
.st182{clip-path:url(#SVGID_239_);}
.st183{clip-path:url(#SVGID_240_);}
.st184{clip-path:url(#SVGID_242_);}
.st185{clip-path:url(#SVGID_243_);}
.st186{clip-path:url(#SVGID_244_);}
.st187{clip-path:url(#SVGID_246_);}
.st188{clip-path:url(#SVGID_247_);}
.st189{clip-path:url(#SVGID_248_);}
.st190{clip-path:url(#SVGID_250_);}
.st191{clip-path:url(#SVGID_251_);}
.st192{clip-path:url(#SVGID_252_);}
.st193{clip-path:url(#SVGID_254_);}
.st194{clip-path:url(#SVGID_255_);}
.st195{clip-path:url(#SVGID_256_);}
</style>
<g
transform="translate(2.1186441)"
id="g61"
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none">
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path37"
d="m 465.94,209.46 c -0.72,3.49 -2.35,7.45 -4.95,12.03 l 21.16,21.16 c 2.83,-5.51 7.14,-15.16 9.47,-26.61 4.76,-23.39 -0.6,-43.02 -15.93,-58.35 -28.01,-28.01 -57.26,-30.61 -94.84,-8.43 -9.04,5.34 -18.64,12.24 -28.55,20.51 -22.58,18.86 -47.88,45.85 -75.22,80.23 17.91,22.52 35,41.95 50.83,57.79 7.26,7.26 14.4,13.91 21.27,19.81 l 18.76,-18.76 c -6.83,-5.78 -13.98,-12.41 -21.32,-19.75 -10.39,-10.39 -21.5,-22.57 -32.99,-36.19 l -2.43,-2.9 2.44,-2.89 c 21.03,-24.91 40.59,-44.8 58.14,-59.09 11.14,-9.08 21.56,-15.98 30.98,-20.53 8.14,-3.92 15.55,-6.11 22.03,-6.49 3.16,-0.19 8.05,-0.15 14,2.3 5.84,2.4 11.79,6.68 18.2,13.09 2.39,2.39 5.82,6.25 7.91,12.08 2.16,6.02 2.51,12.85 1.07,20.88 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path39"
d="M 355.44,371.4 450,276.85 c -5.56,-8.23 -12.85,-16.22 -16.93,-20.49 l -93.37,93.37 c 6.02,7.42 11.29,14.68 15.74,21.67 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path41"
d="m 475.69,342.31 c 15.32,-15.32 20.68,-34.95 15.93,-58.34 -3.3,-16.23 -10.57,-28.84 -11.98,-31.18 l -75.82,-75.82 c -6.95,3.64 -14.5,8.63 -22.52,14.89 l 58.13,58.14 -0.03,0.03 c 4.8,5.01 13.98,15.11 20.13,25.07 3.75,6.08 5.87,11.21 6.46,15.66 1.42,7.98 1.06,14.77 -1.09,20.77 -2.09,5.83 -5.51,9.68 -7.91,12.08 -6.83,6.83 -13.16,11.26 -19.37,13.55 -6.33,2.34 -11.55,2.09 -15.39,1.6 -5.05,-0.63 -10.62,-2.28 -16.58,-4.9 L 385.9,353.6 c 35.17,19.04 63.04,15.46 89.79,-11.29 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path43"
d="m 311.53,35.11 c 5.83,2.09 9.68,5.51 12.08,7.91 6.83,6.83 11.26,13.16 13.55,19.37 2.34,6.33 2.09,11.55 1.6,15.39 -0.63,5.05 -2.28,10.62 -4.9,16.58 L 353.6,114.1 C 372.64,78.93 369.06,51.06 342.31,24.31 326.99,8.99 307.36,3.63 283.97,8.38 c -16.23,3.3 -28.84,10.57 -31.18,11.98 l -75.82,75.82 c 3.64,6.95 8.63,14.5 14.89,22.52 L 250,60.57 l 0.03,0.03 c 5.01,-4.8 15.11,-13.98 25.07,-20.13 6.08,-3.75 11.21,-5.87 15.66,-6.46 7.98,-1.41 14.77,-1.05 20.77,1.1 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path45"
d="m 157.69,24.31 c -28.01,28.01 -30.61,57.26 -8.43,94.84 5.34,9.04 12.24,18.64 20.51,28.54 18.86,22.58 45.85,47.88 80.23,75.22 22.52,-17.91 41.95,-35 57.79,-50.83 7.26,-7.26 13.91,-14.4 19.81,-21.27 l -18.76,-18.76 c -5.78,6.83 -12.41,13.98 -19.75,21.32 -10.39,10.39 -22.57,21.49 -36.19,32.99 l -2.9,2.44 -2.89,-2.44 C 222.2,165.33 202.31,145.77 188.02,128.22 178.94,117.08 172.04,106.66 167.49,97.24 163.57,89.1 161.38,81.69 161,75.21 c -0.19,-3.16 -0.15,-8.05 2.3,-14 2.4,-5.84 6.68,-11.79 13.09,-18.2 2.39,-2.39 6.25,-5.82 12.08,-7.91 6.02,-2.16 12.85,-2.51 20.87,-1.07 l 0.11,0.02 c 3.49,0.72 7.45,2.35 12.03,4.95 L 242.64,17.84 C 237.13,15.01 227.48,10.7 216.02,8.37 192.64,3.63 173.01,8.99 157.69,24.31 Z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path47"
d="m 276.85,50 c -8.23,5.56 -16.22,12.85 -20.49,16.92 l 93.37,93.37 c 7.42,-6.02 14.68,-11.29 21.67,-15.74 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path49"
d="m 24.31,342.31 c 28.01,28.01 57.26,30.61 94.84,8.43 9.04,-5.34 18.64,-12.24 28.54,-20.51 22.58,-18.86 47.88,-45.85 75.22,-80.23 -17.91,-22.52 -35,-41.95 -50.83,-57.79 -7.26,-7.26 -14.4,-13.91 -21.27,-19.81 l -18.76,18.76 c 6.83,5.78 13.98,12.41 21.32,19.75 10.39,10.39 21.5,22.57 32.99,36.19 l 2.44,2.89 -2.44,2.89 c -21.03,24.91 -40.59,44.8 -58.14,59.09 -11.14,9.08 -21.56,15.98 -30.98,20.53 -8.14,3.92 -15.55,6.11 -22.03,6.49 -3.16,0.19 -8.05,0.15 -14,-2.3 -5.84,-2.4 -11.79,-6.68 -18.2,-13.09 -2.39,-2.39 -5.82,-6.25 -7.91,-12.08 -2.16,-6.02 -2.51,-12.85 -1.07,-20.88 l 0.02,-0.11 C 34.77,287.04 36.4,283.08 39,278.5 L 17.84,257.34 c -2.83,5.51 -7.14,15.17 -9.47,26.63 -4.74,23.39 0.62,43.02 15.94,58.34 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path51"
d="m 60.57,250 0.03,-0.03 c -4.8,-5.02 -13.98,-15.11 -20.13,-25.07 -3.75,-6.08 -5.87,-11.21 -6.46,-15.66 -1.42,-7.98 -1.06,-14.77 1.09,-20.77 2.09,-5.83 5.51,-9.68 7.91,-12.08 6.83,-6.83 13.16,-11.26 19.37,-13.55 6.33,-2.34 11.55,-2.09 15.39,-1.6 5.05,0.63 10.62,2.28 16.58,4.9 L 114.09,146.4 C 78.92,127.36 51.05,130.94 24.3,157.69 8.99,173.01 3.63,192.64 8.38,216.03 c 3.3,16.23 10.57,28.84 11.98,31.18 l 75.82,75.82 c 6.95,-3.64 14.5,-8.63 22.52,-14.89 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path53"
d="m 66.93,243.64 93.37,-93.37 c -6.02,-7.42 -11.29,-14.68 -15.74,-21.67 L 50,223.15 c 5.57,8.23 12.85,16.23 16.93,20.49 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path55"
d="m 342.31,475.69 c 28.01,-28.01 30.61,-57.26 8.43,-94.84 -5.34,-9.04 -12.24,-18.64 -20.51,-28.54 -18.86,-22.58 -45.85,-47.88 -80.23,-75.22 -22.52,17.91 -41.95,35 -57.79,50.83 -7.26,7.26 -13.91,14.4 -19.81,21.27 l 18.76,18.76 c 5.78,-6.83 12.41,-13.98 19.75,-21.32 10.39,-10.39 22.57,-21.49 36.19,-32.99 l 2.89,-2.44 2.89,2.44 c 24.91,21.03 44.8,40.59 59.09,58.14 9.08,11.14 15.98,21.56 20.53,30.98 3.93,8.14 6.11,15.56 6.49,22.03 0.19,3.16 0.15,8.05 -2.3,14 -2.4,5.84 -6.68,11.79 -13.09,18.2 -2.39,2.39 -6.25,5.82 -12.08,7.91 -6.02,2.16 -12.85,2.51 -20.88,1.07 l -0.11,-0.02 c -3.49,-0.72 -7.45,-2.35 -12.03,-4.95 l -21.16,21.16 c 5.5,2.83 15.15,7.13 26.59,9.46 23.41,4.76 43.05,-0.6 58.38,-15.93 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path57"
d="M 243.64,433.07 150.27,339.7 c -7.42,6.02 -14.68,11.29 -21.67,15.74 L 223.15,450 c 8.23,-5.57 16.23,-12.85 20.49,-16.93 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path59"
d="m 157.69,475.69 c 15.32,15.32 34.95,20.68 58.33,15.93 16.24,-3.3 28.85,-10.58 31.19,-11.98 l 75.82,-75.82 c -3.64,-6.95 -8.63,-14.5 -14.89,-22.52 L 250,439.43 249.97,439.4 c -5.01,4.8 -15.11,13.98 -25.07,20.13 -6.08,3.75 -11.21,5.87 -15.66,6.46 -7.98,1.42 -14.77,1.06 -20.77,-1.09 -5.83,-2.09 -9.68,-5.51 -12.08,-7.91 -6.83,-6.83 -11.26,-13.16 -13.55,-19.37 -2.34,-6.33 -2.09,-11.55 -1.6,-15.39 0.63,-5.05 2.28,-10.62 4.9,-16.58 L 146.4,385.9 c -19.04,35.17 -15.46,63.04 11.29,89.79 z"
class="st0" />
</g><g
id="g28"
transform="translate(2.1186441)">
<path
class="st0"
d="m 465.94,209.46 c -0.72,3.49 -2.35,7.45 -4.95,12.03 l 21.16,21.16 c 2.83,-5.51 7.14,-15.16 9.47,-26.61 4.76,-23.39 -0.6,-43.02 -15.93,-58.35 -28.01,-28.01 -57.26,-30.61 -94.84,-8.43 -9.04,5.34 -18.64,12.24 -28.55,20.51 -22.58,18.86 -47.88,45.85 -75.22,80.23 17.91,22.52 35,41.95 50.83,57.79 7.26,7.26 14.4,13.91 21.27,19.81 l 18.76,-18.76 c -6.83,-5.78 -13.98,-12.41 -21.32,-19.75 -10.39,-10.39 -21.5,-22.57 -32.99,-36.19 l -2.43,-2.9 2.44,-2.89 c 21.03,-24.91 40.59,-44.8 58.14,-59.09 11.14,-9.08 21.56,-15.98 30.98,-20.53 8.14,-3.92 15.55,-6.11 22.03,-6.49 3.16,-0.19 8.05,-0.15 14,2.3 5.84,2.4 11.79,6.68 18.2,13.09 2.39,2.39 5.82,6.25 7.91,12.08 2.16,6.02 2.51,12.85 1.07,20.88 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="M 355.44,371.4 450,276.85 c -5.56,-8.23 -12.85,-16.22 -16.93,-20.49 l -93.37,93.37 c 6.02,7.42 11.29,14.68 15.74,21.67 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 475.69,342.31 c 15.32,-15.32 20.68,-34.95 15.93,-58.34 -3.3,-16.23 -10.57,-28.84 -11.98,-31.18 l -75.82,-75.82 c -6.95,3.64 -14.5,8.63 -22.52,14.89 l 58.13,58.14 -0.03,0.03 c 4.8,5.01 13.98,15.11 20.13,25.07 3.75,6.08 5.87,11.21 6.46,15.66 1.42,7.98 1.06,14.77 -1.09,20.77 -2.09,5.83 -5.51,9.68 -7.91,12.08 -6.83,6.83 -13.16,11.26 -19.37,13.55 -6.33,2.34 -11.55,2.09 -15.39,1.6 -5.05,-0.63 -10.62,-2.28 -16.58,-4.9 L 385.9,353.6 c 35.17,19.04 63.04,15.46 89.79,-11.29 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 311.53,35.11 c 5.83,2.09 9.68,5.51 12.08,7.91 6.83,6.83 11.26,13.16 13.55,19.37 2.34,6.33 2.09,11.55 1.6,15.39 -0.63,5.05 -2.28,10.62 -4.9,16.58 L 353.6,114.1 C 372.64,78.93 369.06,51.06 342.31,24.31 326.99,8.99 307.36,3.63 283.97,8.38 c -16.23,3.3 -28.84,10.57 -31.18,11.98 l -75.82,75.82 c 3.64,6.95 8.63,14.5 14.89,22.52 L 250,60.57 l 0.03,0.03 c 5.01,-4.8 15.11,-13.98 25.07,-20.13 6.08,-3.75 11.21,-5.87 15.66,-6.46 7.98,-1.41 14.77,-1.05 20.77,1.1 z"
id="path10"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 157.69,24.31 c -28.01,28.01 -30.61,57.26 -8.43,94.84 5.34,9.04 12.24,18.64 20.51,28.54 18.86,22.58 45.85,47.88 80.23,75.22 22.52,-17.91 41.95,-35 57.79,-50.83 7.26,-7.26 13.91,-14.4 19.81,-21.27 l -18.76,-18.76 c -5.78,6.83 -12.41,13.98 -19.75,21.32 -10.39,10.39 -22.57,21.49 -36.19,32.99 l -2.9,2.44 -2.89,-2.44 C 222.2,165.33 202.31,145.77 188.02,128.22 178.94,117.08 172.04,106.66 167.49,97.24 163.57,89.1 161.38,81.69 161,75.21 c -0.19,-3.16 -0.15,-8.05 2.3,-14 2.4,-5.84 6.68,-11.79 13.09,-18.2 2.39,-2.39 6.25,-5.82 12.08,-7.91 6.02,-2.16 12.85,-2.51 20.87,-1.07 l 0.11,0.02 c 3.49,0.72 7.45,2.35 12.03,4.95 L 242.64,17.84 C 237.13,15.01 227.48,10.7 216.02,8.37 192.64,3.63 173.01,8.99 157.69,24.31 Z"
id="path12"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 276.85,50 c -8.23,5.56 -16.22,12.85 -20.49,16.92 l 93.37,93.37 c 7.42,-6.02 14.68,-11.29 21.67,-15.74 z"
id="path14"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 24.31,342.31 c 28.01,28.01 57.26,30.61 94.84,8.43 9.04,-5.34 18.64,-12.24 28.54,-20.51 22.58,-18.86 47.88,-45.85 75.22,-80.23 -17.91,-22.52 -35,-41.95 -50.83,-57.79 -7.26,-7.26 -14.4,-13.91 -21.27,-19.81 l -18.76,18.76 c 6.83,5.78 13.98,12.41 21.32,19.75 10.39,10.39 21.5,22.57 32.99,36.19 l 2.44,2.89 -2.44,2.89 c -21.03,24.91 -40.59,44.8 -58.14,59.09 -11.14,9.08 -21.56,15.98 -30.98,20.53 -8.14,3.92 -15.55,6.11 -22.03,6.49 -3.16,0.19 -8.05,0.15 -14,-2.3 -5.84,-2.4 -11.79,-6.68 -18.2,-13.09 -2.39,-2.39 -5.82,-6.25 -7.91,-12.08 -2.16,-6.02 -2.51,-12.85 -1.07,-20.88 l 0.02,-0.11 C 34.77,287.04 36.4,283.08 39,278.5 L 17.84,257.34 c -2.83,5.51 -7.14,15.17 -9.47,26.63 -4.74,23.39 0.62,43.02 15.94,58.34 z"
id="path16"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 60.57,250 0.03,-0.03 c -4.8,-5.02 -13.98,-15.11 -20.13,-25.07 -3.75,-6.08 -5.87,-11.21 -6.46,-15.66 -1.42,-7.98 -1.06,-14.77 1.09,-20.77 2.09,-5.83 5.51,-9.68 7.91,-12.08 6.83,-6.83 13.16,-11.26 19.37,-13.55 6.33,-2.34 11.55,-2.09 15.39,-1.6 5.05,0.63 10.62,2.28 16.58,4.9 L 114.09,146.4 C 78.92,127.36 51.05,130.94 24.3,157.69 8.99,173.01 3.63,192.64 8.38,216.03 c 3.3,16.23 10.57,28.84 11.98,31.18 l 75.82,75.82 c 6.95,-3.64 14.5,-8.63 22.52,-14.89 z"
id="path18"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 66.93,243.64 93.37,-93.37 c -6.02,-7.42 -11.29,-14.68 -15.74,-21.67 L 50,223.15 c 5.57,8.23 12.85,16.23 16.93,20.49 z"
id="path20"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 342.31,475.69 c 28.01,-28.01 30.61,-57.26 8.43,-94.84 -5.34,-9.04 -12.24,-18.64 -20.51,-28.54 -18.86,-22.58 -45.85,-47.88 -80.23,-75.22 -22.52,17.91 -41.95,35 -57.79,50.83 -7.26,7.26 -13.91,14.4 -19.81,21.27 l 18.76,18.76 c 5.78,-6.83 12.41,-13.98 19.75,-21.32 10.39,-10.39 22.57,-21.49 36.19,-32.99 l 2.89,-2.44 2.89,2.44 c 24.91,21.03 44.8,40.59 59.09,58.14 9.08,11.14 15.98,21.56 20.53,30.98 3.93,8.14 6.11,15.56 6.49,22.03 0.19,3.16 0.15,8.05 -2.3,14 -2.4,5.84 -6.68,11.79 -13.09,18.2 -2.39,2.39 -6.25,5.82 -12.08,7.91 -6.02,2.16 -12.85,2.51 -20.88,1.07 l -0.11,-0.02 c -3.49,-0.72 -7.45,-2.35 -12.03,-4.95 l -21.16,21.16 c 5.5,2.83 15.15,7.13 26.59,9.46 23.41,4.76 43.05,-0.6 58.38,-15.93 z"
id="path22"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="M 243.64,433.07 150.27,339.7 c -7.42,6.02 -14.68,11.29 -21.67,15.74 L 223.15,450 c 8.23,-5.57 16.23,-12.85 20.49,-16.93 z"
id="path24"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 157.69,475.69 c 15.32,15.32 34.95,20.68 58.33,15.93 16.24,-3.3 28.85,-10.58 31.19,-11.98 l 75.82,-75.82 c -3.64,-6.95 -8.63,-14.5 -14.89,-22.52 L 250,439.43 249.97,439.4 c -5.01,4.8 -15.11,13.98 -25.07,20.13 -6.08,3.75 -11.21,5.87 -15.66,6.46 -7.98,1.42 -14.77,1.06 -20.77,-1.09 -5.83,-2.09 -9.68,-5.51 -12.08,-7.91 -6.83,-6.83 -11.26,-13.16 -13.55,-19.37 -2.34,-6.33 -2.09,-11.55 -1.6,-15.39 0.63,-5.05 2.28,-10.62 4.9,-16.58 L 146.4,385.9 c -19.04,35.17 -15.46,63.04 11.29,89.79 z"
id="path26"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#242425;}
.st1{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M436.2,215c-0.6,3-2,6.4-4.3,10.4l18.2,18.2c2.4-4.8,6.2-13.1,8.2-22.9c4.1-20.2-0.5-37.1-13.7-50.3
c-24.2-24.2-49.4-26.4-81.8-7.3c-7.8,4.6-16.1,10.6-24.6,17.7c-19.5,16.3-41.3,39.5-64.9,69.2c15.4,19.4,30.2,36.2,43.8,49.8
c6.3,6.3,12.4,12,18.3,17.1l16.2-16.2c-5.9-5-12.1-10.7-18.4-17c-9-9-18.5-19.5-28.4-31.2l-2.1-2.5l2.1-2.5
c18.1-21.5,35-38.6,50.1-50.9c9.6-7.8,18.6-13.8,26.7-17.7c7-3.4,13.4-5.3,19-5.6c2.7-0.2,6.9-0.1,12.1,2c5,2.1,10.2,5.8,15.7,11.3
c2.1,2.1,5,5.4,6.8,10.4C437.1,202.1,437.5,208,436.2,215L436.2,215z"/>
<path class="st0" d="M340.9,354.7l81.5-81.5c-4.8-7.1-11.1-14-14.6-17.7L327.3,336C332.5,342.4,337.1,348.6,340.9,354.7z"/>
<path class="st0" d="M444.6,329.6c13.2-13.2,17.8-30.1,13.7-50.3c-2.8-14-9.1-24.9-10.3-26.9L382.6,187c-6,3.1-12.5,7.4-19.4,12.8
l50.1,50.1l0,0c4.1,4.3,12.1,13,17.4,21.6c3.2,5.2,5.1,9.7,5.6,13.5c1.2,6.9,0.9,12.7-0.9,17.9c-1.8,5-4.8,8.3-6.8,10.4
c-5.9,5.9-11.3,9.7-16.7,11.7c-5.5,2-10,1.8-13.3,1.4c-4.4-0.5-9.2-2-14.3-4.2l-17,17C397.5,355.7,421.5,352.7,444.6,329.6z"/>
<path class="st0" d="M303.1,64.7c5,1.8,8.3,4.8,10.4,6.8c5.9,5.9,9.7,11.3,11.7,16.7c2,5.5,1.8,10,1.4,13.3
c-0.5,4.4-2,9.2-4.2,14.3l17,17c16.4-30.3,13.3-54.4-9.7-77.4c-13.2-13.2-30.1-17.8-50.3-13.7c-14,2.8-24.9,9.1-26.9,10.3
L187,117.4c3.1,6,7.4,12.5,12.8,19.4L250,86.7l0,0c4.3-4.1,13-12.1,21.6-17.4c5.2-3.2,9.7-5.1,13.5-5.6
C292,62.6,297.9,62.9,303.1,64.7z"/>
<path class="st0" d="M170.4,55.4c-24.2,24.2-26.4,49.4-7.3,81.8c4.6,7.8,10.6,16.1,17.7,24.6c16.3,19.5,39.5,41.3,69.2,64.9
c19.4-15.4,36.2-30.2,49.8-43.8c6.3-6.3,12-12.4,17.1-18.3l-16.2-16.2c-5,5.9-10.7,12.1-17,18.4c-9,9-19.5,18.5-31.2,28.4l-2.5,2.1
l-2.5-2.1c-21.5-18.1-38.6-35-50.9-50.1c-7.8-9.6-13.8-18.6-17.7-26.7c-3.4-7-5.3-13.4-5.6-19c-0.2-2.7-0.1-6.9,2-12.1
c2.1-5,5.8-10.2,11.3-15.7c2.1-2.1,5.4-5,10.4-6.8c5.2-1.9,11.1-2.2,18-0.9l0.1,0c3,0.6,6.4,2,10.4,4.3l18.2-18.2
c-4.8-2.4-13.1-6.2-23-8.2C200.5,37.6,183.6,42.2,170.4,55.4z"/>
<path class="st0" d="M273.2,77.6c-7.1,4.8-14,11.1-17.7,14.6l80.5,80.5c6.4-5.2,12.7-9.7,18.7-13.6L273.2,77.6z"/>
<path class="st0" d="M55.4,329.6c24.2,24.2,49.4,26.4,81.8,7.3c7.8-4.6,16.1-10.6,24.6-17.7c19.5-16.3,41.3-39.5,64.9-69.2
c-15.4-19.4-30.2-36.2-43.8-49.8c-6.3-6.3-12.4-12-18.3-17.1l-16.2,16.2c5.9,5,12.1,10.7,18.4,17c9,9,18.5,19.5,28.4,31.2l2.1,2.5
l-2.1,2.5c-18.1,21.5-35,38.6-50.1,50.9c-9.6,7.8-18.6,13.8-26.7,17.7c-7,3.4-13.4,5.3-19,5.6c-2.7,0.2-6.9,0.1-12.1-2
c-5-2.1-10.2-5.8-15.7-11.3c-2.1-2.1-5-5.4-6.8-10.4c-1.9-5.2-2.2-11.1-0.9-18l0-0.1c0.6-3,2-6.4,4.3-10.4l-18.2-18.2
c-2.4,4.8-6.2,13.1-8.2,23C37.6,299.5,42.2,316.4,55.4,329.6z"/>
<path class="st0" d="M86.7,250L86.7,250c-4.1-4.4-12-13.1-17.3-21.6c-3.2-5.2-5.1-9.7-5.6-13.5c-1.2-6.9-0.9-12.7,0.9-17.9
c1.8-5,4.8-8.3,6.8-10.4c5.9-5.9,11.3-9.7,16.7-11.7c5.5-2,10-1.8,13.3-1.4c4.4,0.5,9.2,2,14.3,4.2l17-17
c-30.3-16.4-54.4-13.3-77.4,9.7c-13.2,13.2-17.8,30.1-13.7,50.3c2.8,14,9.1,24.9,10.3,26.9l65.4,65.4c6-3.1,12.5-7.4,19.4-12.8
L86.7,250z"/>
<path class="st0" d="M92.2,244.5l80.5-80.5c-5.2-6.4-9.7-12.7-13.6-18.7l-81.5,81.5C82.4,233.9,88.6,240.8,92.2,244.5z"/>
<path class="st0" d="M329.6,444.6c24.2-24.2,26.4-49.4,7.3-81.8c-4.6-7.8-10.6-16.1-17.7-24.6c-16.3-19.5-39.5-41.3-69.2-64.9
c-19.4,15.4-36.2,30.2-49.8,43.8c-6.3,6.3-12,12.4-17.1,18.3l16.2,16.2c5-5.9,10.7-12.1,17-18.4c9-9,19.5-18.5,31.2-28.4l2.5-2.1
l2.5,2.1c21.5,18.1,38.6,35,50.9,50.1c7.8,9.6,13.8,18.6,17.7,26.7c3.4,7,5.3,13.4,5.6,19c0.2,2.7,0.1,6.9-2,12.1
c-2.1,5-5.8,10.2-11.3,15.7c-2.1,2.1-5.4,5-10.4,6.8c-5.2,1.9-11.1,2.2-18,0.9l-0.1,0c-3-0.6-6.4-2-10.4-4.3l-18.2,18.2
c4.7,2.4,13.1,6.1,22.9,8.2C299.4,462.4,316.4,457.8,329.6,444.6z"/>
<path class="st0" d="M244.5,407.8L164,327.3c-6.4,5.2-12.7,9.7-18.7,13.6l81.5,81.5C233.9,417.6,240.8,411.4,244.5,407.8z"/>
<path class="st0" d="M483.4,250c2.6-6.3,5.3-14,7.1-22.7c6.3-30.8-1.8-59.2-22.7-80.1c-19.7-19.7-41.7-29.7-65.5-29.7
c-7.5,0-15.1,1-22.8,3c2.9-11.3,3.7-22.3,2.3-33.2c-2.5-19.8-12.3-38.3-29-55.1C336.6,15.9,315.9,7.4,293,7.4
c-6.6,0-13.4,0.7-20.2,2.1c-8.7,1.8-16.5,4.5-22.7,7.1c-6.3-2.6-14.1-5.3-22.8-7.1c-6.9-1.4-13.7-2.1-20.2-2.1
c-22.9,0-43.6,8.6-59.9,24.8c-17.6,17.6-27.5,37.2-29.4,58.1c-0.9,9.9,0,19.9,2.7,30.2c-7.7-2-15.3-3-22.8-3
c-23.8,0-45.8,10-65.5,29.7c-20.9,20.9-28.9,49.3-22.7,80.1c1.8,8.7,4.5,16.5,7.1,22.7c-2.6,6.3-5.3,14.1-7.1,22.8
c-6.2,30.8,1.8,59.2,22.7,80.1c19.7,19.7,41.7,29.7,65.5,29.7c0,0,0,0,0,0c7.5,0,15.1-1,22.8-3c-2.9,11.3-3.7,22.3-2.3,33.2
c2.5,19.8,12.3,38.3,29,55.1c16.2,16.2,36.9,24.8,59.8,24.8c0,0,0,0,0,0c6.6,0,13.4-0.7,20.2-2.1c8.7-1.8,16.5-4.4,22.7-7.1
c6.3,2.6,14,5.3,22.7,7.1c6.9,1.4,13.7,2.1,20.3,2.1c0,0,0,0,0,0c22.9,0,43.6-8.6,59.9-24.8c17.6-17.6,27.5-37.2,29.4-58.1
c0.9-9.9,0-19.9-2.7-30.2c7.7,2,15.3,3,22.8,3c0,0,0,0,0,0c23.8,0,45.8-10,65.5-29.7c20.9-20.9,29-49.3,22.7-80.1
C488.8,264,486.1,256.3,483.4,250z"/>
<path class="st0" d="M170.4,444.6c13.2,13.2,30.1,17.8,50.3,13.7c14-2.8,24.9-9.1,26.9-10.3l65.4-65.4c-3.1-6-7.4-12.5-12.8-19.4
L250,413.3l0,0c-4.3,4.1-13,12.1-21.6,17.4c-5.2,3.2-9.7,5.1-13.5,5.6c-6.9,1.2-12.7,0.9-17.9-0.9c-5-1.8-8.3-4.8-10.4-6.8
c-5.9-5.9-9.7-11.3-11.7-16.7c-2-5.5-1.8-10-1.4-13.3c0.5-4.4,2-9.2,4.2-14.3l-17-17C144.3,397.5,147.3,421.5,170.4,444.6z"/>
</g>
<g>
<path class="st1" d="M436.2,215c-0.6,3-2,6.4-4.3,10.4l18.2,18.2c2.4-4.8,6.2-13.1,8.2-22.9c4.1-20.2-0.5-37.1-13.7-50.3
c-24.2-24.2-49.4-26.4-81.8-7.3c-7.8,4.6-16.1,10.6-24.6,17.7c-19.5,16.3-41.3,39.5-64.9,69.2c15.4,19.4,30.2,36.2,43.8,49.8
c6.3,6.3,12.4,12,18.3,17.1l16.2-16.2c-5.9-5-12.1-10.7-18.4-17c-9-9-18.5-19.5-28.4-31.2l-2.1-2.5l2.1-2.5
c18.1-21.5,35-38.6,50.1-50.9c9.6-7.8,18.6-13.8,26.7-17.7c7-3.4,13.4-5.3,19-5.6c2.7-0.2,6.9-0.1,12.1,2c5,2.1,10.2,5.8,15.7,11.3
c2.1,2.1,5,5.4,6.8,10.4C437.1,202.1,437.5,208,436.2,215L436.2,215z"/>
<path class="st1" d="M340.9,354.7l81.5-81.5c-4.8-7.1-11.1-14-14.6-17.7L327.3,336C332.5,342.4,337.1,348.6,340.9,354.7z"/>
<path class="st1" d="M444.6,329.6c13.2-13.2,17.8-30.1,13.7-50.3c-2.8-14-9.1-24.9-10.3-26.9L382.6,187c-6,3.1-12.5,7.4-19.4,12.8
l50.1,50.1l0,0c4.1,4.3,12.1,13,17.4,21.6c3.2,5.2,5.1,9.7,5.6,13.5c1.2,6.9,0.9,12.7-0.9,17.9c-1.8,5-4.8,8.3-6.8,10.4
c-5.9,5.9-11.3,9.7-16.7,11.7c-5.5,2-10,1.8-13.3,1.4c-4.4-0.5-9.2-2-14.3-4.2l-17,17C397.5,355.7,421.5,352.7,444.6,329.6z"/>
<path class="st1" d="M303.1,64.7c5,1.8,8.3,4.8,10.4,6.8c5.9,5.9,9.7,11.3,11.7,16.7c2,5.5,1.8,10,1.4,13.3
c-0.5,4.4-2,9.2-4.2,14.3l17,17c16.4-30.3,13.3-54.4-9.7-77.4c-13.2-13.2-30.1-17.8-50.3-13.7c-14,2.8-24.9,9.1-26.9,10.3
L187,117.4c3.1,6,7.4,12.5,12.8,19.4L250,86.7l0,0c4.3-4.1,13-12.1,21.6-17.4c5.2-3.2,9.7-5.1,13.5-5.6
C292,62.6,297.9,62.9,303.1,64.7z"/>
<path class="st1" d="M170.4,55.4c-24.2,24.2-26.4,49.4-7.3,81.8c4.6,7.8,10.6,16.1,17.7,24.6c16.3,19.5,39.5,41.3,69.2,64.9
c19.4-15.4,36.2-30.2,49.8-43.8c6.3-6.3,12-12.4,17.1-18.3l-16.2-16.2c-5,5.9-10.7,12.1-17,18.4c-9,9-19.5,18.5-31.2,28.4l-2.5,2.1
l-2.5-2.1c-21.5-18.1-38.6-35-50.9-50.1c-7.8-9.6-13.8-18.6-17.7-26.7c-3.4-7-5.3-13.4-5.6-19c-0.2-2.7-0.1-6.9,2-12.1
c2.1-5,5.8-10.2,11.3-15.7c2.1-2.1,5.4-5,10.4-6.8c5.2-1.9,11.1-2.2,18-0.9l0.1,0c3,0.6,6.4,2,10.4,4.3l18.2-18.2
c-4.8-2.4-13.1-6.2-23-8.2C200.5,37.6,183.6,42.2,170.4,55.4z"/>
<path class="st1" d="M273.2,77.6c-7.1,4.8-14,11.1-17.7,14.6l80.5,80.5c6.4-5.2,12.7-9.7,18.7-13.6L273.2,77.6z"/>
<path class="st1" d="M55.4,329.6c24.2,24.2,49.4,26.4,81.8,7.3c7.8-4.6,16.1-10.6,24.6-17.7c19.5-16.3,41.3-39.5,64.9-69.2
c-15.4-19.4-30.2-36.2-43.8-49.8c-6.3-6.3-12.4-12-18.3-17.1l-16.2,16.2c5.9,5,12.1,10.7,18.4,17c9,9,18.5,19.5,28.4,31.2l2.1,2.5
l-2.1,2.5c-18.1,21.5-35,38.6-50.1,50.9c-9.6,7.8-18.6,13.8-26.7,17.7c-7,3.4-13.4,5.3-19,5.6c-2.7,0.2-6.9,0.1-12.1-2
c-5-2.1-10.2-5.8-15.7-11.3c-2.1-2.1-5-5.4-6.8-10.4c-1.9-5.2-2.2-11.1-0.9-18l0-0.1c0.6-3,2-6.4,4.3-10.4l-18.2-18.2
c-2.4,4.8-6.2,13.1-8.2,23C37.6,299.5,42.2,316.4,55.4,329.6z"/>
<path class="st1" d="M86.7,250L86.7,250c-4.1-4.4-12-13.1-17.3-21.6c-3.2-5.2-5.1-9.7-5.6-13.5c-1.2-6.9-0.9-12.7,0.9-17.9
c1.8-5,4.8-8.3,6.8-10.4c5.9-5.9,11.3-9.7,16.7-11.7c5.5-2,10-1.8,13.3-1.4c4.4,0.5,9.2,2,14.3,4.2l17-17
c-30.3-16.4-54.4-13.3-77.4,9.7c-13.2,13.2-17.8,30.1-13.7,50.3c2.8,14,9.1,24.9,10.3,26.9l65.4,65.4c6-3.1,12.5-7.4,19.4-12.8
L86.7,250z"/>
<path class="st1" d="M92.2,244.5l80.5-80.5c-5.2-6.4-9.7-12.7-13.6-18.7l-81.5,81.5C82.4,233.9,88.6,240.8,92.2,244.5z"/>
<path class="st1" d="M329.6,444.6c24.2-24.2,26.4-49.4,7.3-81.8c-4.6-7.8-10.6-16.1-17.7-24.6c-16.3-19.5-39.5-41.3-69.2-64.9
c-19.4,15.4-36.2,30.2-49.8,43.8c-6.3,6.3-12,12.4-17.1,18.3l16.2,16.2c5-5.9,10.7-12.1,17-18.4c9-9,19.5-18.5,31.2-28.4l2.5-2.1
l2.5,2.1c21.5,18.1,38.6,35,50.9,50.1c7.8,9.6,13.8,18.6,17.7,26.7c3.4,7,5.3,13.4,5.6,19c0.2,2.7,0.1,6.9-2,12.1
c-2.1,5-5.8,10.2-11.3,15.7c-2.1,2.1-5.4,5-10.4,6.8c-5.2,1.9-11.1,2.2-18,0.9l-0.1,0c-3-0.6-6.4-2-10.4-4.3l-18.2,18.2
c4.7,2.4,13.1,6.1,22.9,8.2C299.4,462.4,316.4,457.8,329.6,444.6z"/>
<path class="st1" d="M244.5,407.8L164,327.3c-6.4,5.2-12.7,9.7-18.7,13.6l81.5,81.5C233.9,417.6,240.8,411.4,244.5,407.8z"/>
<path class="st1" d="M170.4,444.6c13.2,13.2,30.1,17.8,50.3,13.7c14-2.8,24.9-9.1,26.9-10.3l65.4-65.4c-3.1-6-7.4-12.5-12.8-19.4
L250,413.3l0,0c-4.3,4.1-13,12.1-21.6,17.4c-5.2,3.2-9.7,5.1-13.5,5.6c-6.9,1.2-12.7,0.9-17.9-0.9c-5-1.8-8.3-4.8-10.4-6.8
c-5.9-5.9-9.7-11.3-11.7-16.7c-2-5.5-1.8-10-1.4-13.3c0.5-4.4,2-9.2,4.2-14.3l-17-17C144.3,397.5,147.3,421.5,170.4,444.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

20
build-release.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/sh
if [ -z "$1" ]; then
echo "build-release.sh [android|linux|macos|windows]"
exit 1
fi
if [ -f "VERSION" ]; then
VERSION=`cat VERSION`
else
VERSION=`git describe --tags --abbrev=1`
fi
if [ -f "BUILDDATE" ]; then
BUILDDATE=`cat BUILDDATE`
else
BUILDDATE=`date +%G-%m-%d-%H-%M`
fi
flutter build $1 --dart-define BUILD_VER=$VERSION --dart-define BUILD_DATE=$BUILDDATE

BIN
cwtch.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 51 KiB

6
fetch-libcwtch-go-macos.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
VERSION=`cat LIBCWTCH-GO-MACOS.version`
echo $VERSION
curl https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.dylib --output libCwtch.dylib

View File

@ -5,5 +5,3 @@ echo $VERSION
wget https://build.openprivacy.ca/files/libCwtch-go-$VERSION/cwtch.aar -O android/cwtch/cwtch.aar
wget https://build.openprivacy.ca/files/libCwtch-go-$VERSION/libCwtch.so -O linux/libCwtch.so
# wget https://build.openprivacy.ca/files/libCwtch-go-$VERSION/libCwtch.dll -O windows/libCwtch.dll

7
fetch-tor-macos.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
cd macos
curl https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-macos-0.4.6.7.tar.gz --output tor.tar.gz
tar -xzf tor.tar.gz
chmod a+x Tor/tor.real
cd ..

View File

@ -1,6 +1,6 @@
Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.0.18/tor-win64-0.4.5.9.zip -OutFile tor.zip
Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '72764eb07ad8ab511603aba0734951ca003989f5f4686af91ba220217b4a8a4bcc5f571b59f52c847932f6efedf847b111621983050fcddbb8099d43ca66fb07' ) { Write-Error 'tor.zip sha512sum mismatch' }
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }
Expand-Archive -Path tor.zip -DestinationPath Tor

View File

@ -1,19 +1,23 @@
import 'package:flutter/src/services/text_input.dart';
// To handle profiles that are "unencrypted" (i.e don't require a password to open) we currently create a profile with a defacto, hardcoded password.
// Details: https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
const DefaultPassword = "be gay do crime";
abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<void> Start();
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground();
// ignore: non_constant_identifier_names
void SelectProfile(String onion);
// ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass);
// ignore: non_constant_identifier_names
void LoadProfiles(String pass);
// ignore: non_constant_identifier_names
void DeleteProfile(String onion, String pass);
void DeleteProfile(String profile, String pass);
// ignore: non_constant_identifier_names
void ChangePassword(String profile, String pass, String newpass, String newpassAgain);
// ignore: non_constant_identifier_names
void ResetTor();
@ -25,38 +29,79 @@ abstract class Cwtch {
void SendAppEvent(String jsonEvent);
// ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle);
void AcceptContact(String profileOnion, int contactHandle);
// ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle);
void BlockContact(String profileOnion, int contactHandle);
// ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index);
// ignore: non_constant_identifier_names
Future<dynamic> GetMessageByContentHash(String profile, String handle, String contentHash);
// ignore: non_constant_identifier_names
void UpdateMessageFlags(String profile, String handle, int index, int flags);
// ignore: non_constant_identifier_names
void SendMessage(String profile, String handle, String message);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, String handle, String target);
Future<dynamic> GetMessage(String profile, int handle, int index);
// ignore: non_constant_identifier_names
void LeaveConversation(String profile, String handle);
Future<dynamic> GetMessageByID(String profile, int handle, int index);
// ignore: non_constant_identifier_names
Future<dynamic> GetMessageByContentHash(String profile, int handle, String contentHash);
// ignore: non_constant_identifier_names
void SendMessage(String profile, int handle, String message);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, int handle, int target);
// ignore: non_constant_identifier_names
void ShareFile(String profile, int handle, String filepath);
// ignore: non_constant_identifier_names
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
// android-only
// ignore: non_constant_identifier_names
void CreateDownloadableFile(String profile, int handle, String filenameSuggestion, String filekey);
// ignore: non_constant_identifier_names
void CheckDownloadStatus(String profile, String fileKey);
// ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profile, int handle, String filekey);
// android-only
// ignore: non_constant_identifier_names
void ExportPreviewedFile(String sourceFile, String suggestion);
// ignore: non_constant_identifier_names
void ArchiveConversation(String profile, int handle);
// ignore: non_constant_identifier_names
void DeleteContact(String profile, int handle);
// ignore: non_constant_identifier_names
void CreateGroup(String profile, String server, String groupName);
// ignore: non_constant_identifier_names
void LeaveGroup(String profile, String groupID);
// ignore: non_constant_identifier_names
void ImportBundle(String profile, String bundle);
// ignore: non_constant_identifier_names
void SetGroupAttribute(String profile, String groupHandle, String key, String value);
void SetProfileAttribute(String profile, String key, String val);
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle);
void SetConversationAttribute(String profile, int conversation, String key, String val);
// ignore: non_constant_identifier_names
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val);
// ignore: non_constant_identifier_names
void LoadServers(String password);
// ignore: non_constant_identifier_names
void CreateServer(String password, String description, bool autostart);
// ignore: non_constant_identifier_names
void DeleteServer(String serverOnion, String password);
// ignore: non_constant_identifier_names
void LaunchServers();
// ignore: non_constant_identifier_names
void LaunchServer(String serverOnion);
// ignore: non_constant_identifier_names
void StopServer(String serverOnion);
// ignore: non_constant_identifier_names
void StopServers();
// ignore: non_constant_identifier_names
void DestroyServers();
// ignore: non_constant_identifier_names
void SetServerAttribute(String serverOnion, String key, String val);
// ignore: non_constant_identifier_names
void Shutdown();
// non-ffi
String defaultDownloadPath();
void dispose();
}

View File

@ -1,5 +1,7 @@
import 'dart:convert';
import 'package:cwtch/main.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/notification_manager.dart';
import 'package:provider/provider.dart';
@ -20,17 +22,21 @@ class CwtchNotifier {
late TorStatus torStatus;
late NotificationsManager notificationManager;
late AppState appState;
late ServerListState serverListState;
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN) {
CwtchNotifier(
ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) {
profileCN = pcn;
settings = settingsCN;
error = errorCN;
torStatus = torStatusCN;
notificationManager = notificationManagerP;
appState = appStateCN;
serverListState = serverListStateCN;
}
void handleMessage(String type, dynamic data) {
//EnvironmentConfig.debugLog("NewEvent $type $data");
switch (type) {
case "CwtchStarted":
appState.SetCwtchInit();
@ -39,38 +45,63 @@ class CwtchNotifier {
appState.SetAppError(data["Error"]);
break;
case "NewPeer":
EnvironmentConfig.debugLog("NewPeer $data");
// if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta...
profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword");
break;
case "PeerCreated":
case "ContactCreated":
EnvironmentConfig.debugLog("NewServer $data");
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(
data["ProfileOnion"],
int.parse(data["ConversationID"]),
data["RemotePeer"],
nickname: data["nick"],
status: data["status"],
imagePath: data["picture"],
isBlocked: data["authorization"] == "blocked",
isInvitation: data["authorization"] == "unknown",
authorization: stringToContactAuthorization(data["authorization"]),
savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"],
numMessages: int.parse(data["numMessages"]),
numUnread: int.parse(data["unread"]),
isGroup: data["isGroup"] == true,
server: data["groupServer"],
archived: data["isArchived"] == true,
lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
));
break;
case "NewServer":
EnvironmentConfig.debugLog("NewServer $data");
serverListState.add(data["Onion"], data["ServerBundle"], data["Running"] == "true", data["Description"], data["Autostart"] == "true", data["StorageType"] == "storage-password");
break;
case "ServerIntentUpdate":
EnvironmentConfig.debugLog("ServerIntentUpdate $data");
var server = serverListState.getServer(data["Identity"]);
if (server != null) {
server.setRunning(data["Intent"] == "running");
}
break;
case "ServerStatsUpdate":
EnvironmentConfig.debugLog("ServerStatsUpdate $data");
var totalMessages = int.parse(data["TotalMessages"]);
var connections = int.parse(data["Connections"]);
serverListState.updateServerStats(data["Identity"], totalMessages, connections);
break;
case "GroupCreated":
// Retrieve Server Status from Cache...
String status = "";
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
if (serverInfoState != null) {
status = serverInfoState.status;
}
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["GroupID"],
isInvitation: false, imagePath: data["PicturePath"], nickname: data["GroupName"], status: status, server: data["GroupServer"], isGroup: true, lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(int.parse(data["ConversationID"])) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["GroupID"],
authorization: ContactAuthorization.approved,
imagePath: data["PicturePath"],
nickname: data["GroupName"],
status: status,
server: data["GroupServer"],
isGroup: true,
lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(int.parse(data["ConversationID"]), DateTime.now());
}
break;
case "PeerDeleted":
@ -78,21 +109,26 @@ class CwtchNotifier {
// todo standarize
error.handleUpdate("deleteprofile.success");
break;
case "ServerDeleted":
error.handleUpdate("deletedserver." + data["Status"]);
if (data["Status"] == "success") {
serverListState.delete(data["Identity"]);
}
break;
case "DeleteContact":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["RemotePeer"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["ConversationID"]);
break;
case "DeleteGroup":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["GroupID"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["ConversationID"]);
break;
case "PeerStateChange":
ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]);
ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
if (contact != null) {
if (data["ConnectionState"] != null) {
contact.status = data["ConnectionState"];
}
if (data["authorization"] != null) {
contact.isInvitation = data["authorization"] == "unknown";
contact.isBlocked = data["authorization"] == "blocked";
contact.authorization = stringToContactAuthorization(data["authorization"]);
}
// contact.[status/isBlocked] might change the list's sort order
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
@ -100,81 +136,107 @@ class CwtchNotifier {
break;
case "NewMessageFromPeer":
notificationManager.notify("New Message From Peer!");
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++;
var identifier = int.parse(data["ConversationID"]);
var messageID = int.parse(data["Index"]);
var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var isAuto = data['Auto'] == "true";
// We might not have received a contact created for this contact yet...
// In that case the **next** event we receive will actually update these values...
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier) != null) {
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++;
} else {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now());
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data["Data"]);
// We only ever see messages from authenticated peers.
// If the contact is marked as offline then override this - can happen when the contact is removed from the front
// end during syncing.
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.isOnline() == false) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.status = "Authenticated";
}
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
break;
case "PeerAcknowledgement":
// We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end...
break;
case "IndexedAcknowledgement":
var idx = data["Index"];
var conversation = int.parse(data["ConversationID"]);
var messageID = int.parse(data["Index"]);
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation);
// We return -1 for protocol message acks if there is no message
if (idx == "-1") break;
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx);
if (messageID == -1) break;
var key = contact!.getMessageKeyOrFail(conversation, messageID);
if (key == null) break;
try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break;
message.ackd = true;
// We only ever see acks from authenticated peers.
// If the contact is marked as offline then override this - can happen when the contact is removed from the front
// end during syncing.
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.isOnline() == false) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.status = "Authenticated";
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.ackCache(messageID);
} catch (e) {
// ignore, we received an ack for a message that hasn't loaded onto the screen yet...
// the protocol was faster than the ui....yay?
// ignore, most likely cause is the key got optimized out...
}
break;
case "NewMessageFromGroup":
var identifier = int.parse(data["ConversationID"]);
if (data["ProfileOnion"] != data["RemotePeer"]) {
//if not currently open
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
var idx = int.parse(data["Index"]);
var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages;
var isAuto = data['Auto'] == "true";
// Only bother to do anything if we know about the group and the provided index is greater than our current total...
if (currentTotal != null && idx >= currentTotal) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"]);
//if not currently open
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++;
} else {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
}
// TODO: There are 2 timestamps associated with a new group message - time sent and time received.
// Sent refers to the time a profile alleges they sent a message
// Received refers to the time we actually saw the message from the server
// These can obviously be very different for legitimate reasons.
// We also maintain a relative hash-link through PreviousMessageSignature which is the ground truth for
// order.
// In the future we will want to combine these 3 ordering mechanisms into a cohesive view of the timeline
// For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
// and `local now`.
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, timestampSent.toLocal());
notificationManager.notify("New Message From Group!");
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
notificationManager.notify("New Message From Group!");
} else {
// from me (already displayed - do not update counter)
var idx = data["Signature"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx);
if (key == null) break;
try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break;
message.ackd = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
// This is dealt with by IndexedAcknowledgment
EnvironmentConfig.debugLog("new message from group from yourself - this should not happen");
}
break;
case "MessageCounterResync":
var contactHandle = data["RemotePeer"];
if (contactHandle == null || contactHandle == "") contactHandle = data["GroupID"];
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = int.parse(data["Data"]);
break;
case "IndexedFailure":
EnvironmentConfig.debugLog("IndexedFailure");
var idx = data["Index"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx);
try {
var message = Provider.of<MessageMetadata>(key!.currentContext!, listen: false);
message.error = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
break;
case "SendMessageToGroupError":
// from me (already displayed - do not update counter)
EnvironmentConfig.debugLog("SendMessageToGroupError");
var idx = data["Signature"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx);
if (key == null) break;
try {
var identifier = int.parse(data["ConversationID"]);
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
var idx = int.parse(data["Index"]);
var key = contact?.getMessageKeyOrFail(contact.identifier, idx);
if (key != null) {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break;
message.error = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
break;
case "AppError":
@ -189,8 +251,8 @@ class CwtchNotifier {
case "UpdateGlobalSettings":
settings.handleUpdate(jsonDecode(data["Data"]));
break;
case "SetAttribute":
if (data["Key"] == "public.name") {
case "UpdatedProfileAttribute":
if (data["Key"] == "public.profile.name") {
profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"];
} else {
EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}");
@ -212,7 +274,6 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]);
break;
case "NewGroup":
EnvironmentConfig.debugLog("new group");
String invite = data["GroupInvite"].toString();
if (invite.startsWith("torv3")) {
String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5)));
@ -220,33 +281,27 @@ class CwtchNotifier {
// Retrieve Server Status from Cache...
String status = "";
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]);
RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]);
if (serverInfoState != null) {
status = serverInfoState.status;
}
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"],
isInvitation: false,
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(groupInvite["GroupID"]) == null) {
var identifier = int.parse(data["ConversationID"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"],
authorization: ContactAuthorization.approved,
imagePath: data["PicturePath"],
nickname: groupInvite["GroupName"],
server: groupInvite["ServerHost"],
status: status,
isGroup: true,
lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(groupInvite["GroupID"], DateTime.now());
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(0)));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.fromMillisecondsSinceEpoch(0));
}
}
break;
case "AcceptGroupInvite":
EnvironmentConfig.debugLog("accept group invite");
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.isInvitation = false;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
break;
case "ServerStateChange":
// Update the Server Cache
EnvironmentConfig.debugLog("server state changes $data");
profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) {
if (contact.isGroup == true && contact.server == data["GroupServer"]) {
@ -255,25 +310,47 @@ class CwtchNotifier {
});
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
break;
case "SetGroupAttribute":
if (data["Key"] == "local.name") {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.nickname = data["Data"];
case "NewRetValMessageFromPeer":
if (data["Path"] == "profile.name") {
if (data["Data"].toString().trim().length > 0) {
// Update locally on the UI...
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
}
}
} else if (data['Path'] == "profile.picture") {
// Not yet..
} else {
EnvironmentConfig.debugLog("unhandled set group attribute event: ${data['Key']}");
EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
}
break;
case "NewRetValMessageFromPeer":
if (data["Path"] == "name") {
// Update locally on the UI...
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.nickname = data["Data"];
}
} else {
EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}");
case "ManifestSizeReceived":
if (!profileCN.getProfile(data["ProfileOnion"])!.downloadActive(data["FileKey"])) {
profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], 0, 1);
}
break;
case "ManifestSaved":
profileCN.getProfile(data["ProfileOnion"])?.downloadMarkManifest(data["FileKey"]);
break;
case "FileDownloadProgressUpdate":
var progress = int.parse(data["Progress"]);
profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], progress, int.parse(data["FileSizeInChunks"]));
// progress == -1 is a "download was interrupted" message and should contain a path
if (progress < 0) {
profileCN.getProfile(data["ProfileOnion"])?.downloadSetPath(data["FileKey"], data["FilePath"]);
}
break;
case "FileDownloaded":
profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]);
break;
case "ImportingProfileEvent":
break;
case "StartingStorageMigration":
appState.SetModalState(ModalState.storageMigration);
break;
case "DoneStorageMigration":
appState.SetModalState(ModalState.none);
break;
default:
EnvironmentConfig.debugLog("unhandled event: $type");
}

View File

@ -4,7 +4,6 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:io' show Platform;
import 'package:cwtch/cwtch/cwtchNotifier.dart';
import 'package:flutter/src/services/text_input.dart';
import 'package:path/path.dart' as path;
import 'package:ffi/ffi.dart';
@ -12,6 +11,9 @@ import 'package:cwtch/cwtch/cwtch.dart';
import '../config.dart';
import "package:path/path.dart" show dirname, join;
import 'dart:io' show Platform;
/////////////////////
/// Cwtch API ///
/////////////////////
@ -22,6 +24,9 @@ typedef StartCwtchFn = int Function(Pointer<Utf8> dir, int len, Pointer<Utf8> to
typedef void_from_void_funtion = Void Function();
typedef VoidFromVoidFunction = void Function();
typedef free_function = Void Function(Pointer<Utf8>);
typedef FreeFn = void Function(Pointer<Utf8>);
typedef void_from_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
@ -31,11 +36,15 @@ typedef VoidFromStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer
typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
// DownloadFile
typedef void_from_string_int_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringStringStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64);
typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
typedef access_cwtch_eventbus_function = Void Function();
typedef NextEventFn = void Function();
typedef void_from_string_string_byte_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int8);
typedef VoidFromStringStringByteFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
typedef string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length);
typedef StringFn = void Function(Pointer<Utf8> dir, int);
@ -43,43 +52,76 @@ typedef StringFn = void Function(Pointer<Utf8> dir, int);
typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef get_json_blob_void_function = Pointer<Utf8> Function();
typedef GetJsonBlobVoidFn = Pointer<Utf8> Function();
typedef string_int_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Int32 handle);
typedef VoidFromStringIntFn = void Function(Pointer<Utf8>, int, int);
typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len);
//func NumMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int) (n C.int) {
typedef get_int_from_str_str_function = Int32 Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef GetIntFromStrStrFn = int Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
//func GetMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, message_index C.int) *C.char {
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
typedef get_json_blob_from_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int);
typedef get_json_blob_from_str_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrIntStringFn = Pointer<Utf8> Function(
Pointer<Utf8>,
int,
int,
Pointer<Utf8>,
int,
);
// func c_GetMessagesByContentHash(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, contenthash_ptr *C.char, contenthash_len C.int) *C.char
typedef get_json_blob_from_str_str_str_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int);
typedef void_from_string_int_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_int_int_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntIntIntStringStringFn = void Function(Pointer<Utf8>, int, int, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Int32, Int32);
typedef VoidFromStringIntIntFn = void Function(Pointer<Utf8>, int, int, int);
typedef appbus_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> Function();
const String UNSUPPORTED_OS = "unsupported-os";
class CwtchFfi implements Cwtch {
late DynamicLibrary library;
late CwtchNotifier cwtchNotifier;
late Isolate cwtchIsolate;
ReceivePort _receivePort = ReceivePort();
CwtchFfi(CwtchNotifier _cwtchNotifier) {
static String getLibraryPath() {
if (Platform.isWindows) {
library = DynamicLibrary.open("libCwtch.dll");
return "libCwtch.dll";
} else if (Platform.isLinux) {
library = DynamicLibrary.open("libCwtch.so");
return "libCwtch.so";
} else if (Platform.isMacOS) {
print(dirname(Platform.script.path));
return "libCwtch.dylib";
} else {
return UNSUPPORTED_OS;
}
}
CwtchFfi(CwtchNotifier _cwtchNotifier) {
String libraryPath = getLibraryPath();
if (libraryPath == UNSUPPORTED_OS) {
print("OS ${Platform.operatingSystem} not supported by cwtch/ffi");
// emergency, ideally the app stays on splash and just posts the error till user closes
exit(0);
}
library = DynamicLibrary.open(libraryPath);
cwtchNotifier = _cwtchNotifier;
}
@ -88,8 +130,9 @@ class CwtchFfi implements Cwtch {
String home = "";
String bundledTor = "";
Map<String, String> envVars = Platform.environment;
String cwtchDir = "";
if (Platform.isLinux) {
home = envVars['HOME']!;
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, ".cwtch");
if (await File("linux/tor").exists()) {
bundledTor = "linux/tor";
} else if (await File("lib/tor").exists()) {
@ -102,14 +145,50 @@ class CwtchFfi implements Cwtch {
bundledTor = "tor";
}
} else if (Platform.isWindows) {
home = envVars['UserProfile']!;
cwtchDir = envVars['CWTCH_DIR'] ?? path.join(envVars['UserProfile']!, ".cwtch");
bundledTor = "Tor\\Tor\\tor.exe";
} else if (Platform.isMacOS) {
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, "Library/Application Support/Cwtch");
if (await File("Cwtch.app/Contents/MacOS/Tor/tor.real").exists()) {
bundledTor = "Cwtch.app/Contents/MacOS/Tor/tor.real";
} else if (await File("/Applications/Cwtch.app/Contents/MacOS/Tor/tor.real").exists()) {
bundledTor = "/Applications/Cwtch.app/Contents/MacOS/Tor/tor.real";
} else if (await File("/Volumes/Cwtch/Cwtch.app/Contents/MacOS/Tor/tor.real").exists()) {
bundledTor = "/Volumes/Cwtch/Cwtch.app/Contents/MacOS/Tor/tor.real";
} else if (await File("/Applications/Tor Browser.app/Contents/MacOS/Tor/tor.real").exists()) {
bundledTor = "/Applications/Tor Browser.app/Contents/MacOS/Tor/tor.real";
print("We couldn't find Tor in the Cwtch app directory, however we can fall back to the Tor Browser binary");
} else {
var splitPath = path.split(dirname(Platform.script.path));
if (splitPath[0] == "/" && splitPath[1] == "Applications") {
var appName = splitPath[2];
print("We're running in /Applications in a non standard app name: $appName");
if (await File("/Applications/$appName/Contents/MacOS/Tor/tor.real").exists()) {
bundledTor = "/Applications/$appName/Contents/MacOS/Tor/tor.real";
}
}
}
}
// the first Cwtch MacOS release (1.2) accidently was a dev build
// we need to temporarily remedy this for a release or two then delete
// if macOs and release build and no profile and is dev profile
// copy dev profile to release profile
if (Platform.isMacOS && EnvironmentConfig.BUILD_VER != dev_version) {
var devProfileExists = await Directory(path.join(cwtchDir, "dev", "profiles")).exists();
var releaseProfileExists = await Directory(path.join(cwtchDir, "profiles")).exists();
if (devProfileExists && !releaseProfileExists) {
print("MacOS one time dev -> release profile migration...");
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "profiles"), cwtchDir]);
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "SALT"), cwtchDir]);
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "ui.globals"), cwtchDir]);
}
}
var cwtchDir = envVars['CWTCH_HOME'] ?? path.join(home, ".cwtch");
if (EnvironmentConfig.BUILD_VER == dev_version) {
cwtchDir = path.join(cwtchDir, "dev");
}
print("StartCwtch( cwtchdir: $cwtchDir, torPath: $bundledTor )");
var startCwtchC = library.lookup<NativeFunction<start_cwtch_function>>("c_StartCwtch");
@ -138,9 +217,7 @@ class CwtchFfi implements Cwtch {
// Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events
@override
void dispose() {
if (cwtchIsolate != null) {
cwtchIsolate.kill(priority: Isolate.immediate);
}
cwtchIsolate.kill(priority: Isolate.immediate);
}
// Entry point for an isolate to listen to a stream of events pulled from libcwtch-go and return them on the sendPort
@ -154,20 +231,27 @@ class CwtchFfi implements Cwtch {
// Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it
static Stream<String> pollAppbusEvents() async* {
late DynamicLibrary library;
if (Platform.isWindows) {
library = DynamicLibrary.open("libCwtch.dll");
} else if (Platform.isLinux) {
library = DynamicLibrary.open("libCwtch.so");
}
late DynamicLibrary library = DynamicLibrary.open(getLibraryPath());
var getAppbusEventC = library.lookup<NativeFunction<appbus_events_function>>("c_GetAppBusEvent");
// ignore: non_constant_identifier_names
final GetAppbusEvent = getAppbusEventC.asFunction<AppbusEventsFn>();
while (true) {
// Embedded Version of _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved
var free = library.lookup<NativeFunction<free_function>>("c_FreePointer");
final Free = free.asFunction<FreeFn>();
// ignore: non_constant_identifier_names
final GetAppBusEvent = () {
// ignore: non_constant_identifier_names
Pointer<Utf8> result = GetAppbusEvent();
String event = result.toDartString();
Free(result);
return event;
};
while (true) {
final event = GetAppBusEvent();
if (event.startsWith("{\"EventType\":\"Shutdown\"")) {
print("Shutting down isolate thread: $event");
@ -177,15 +261,6 @@ class CwtchFfi implements Cwtch {
}
}
// ignore: non_constant_identifier_names
void SelectProfile(String onion) async {
var selectProfileC = library.lookup<NativeFunction<get_json_blob_string_function>>("c_SelectProfile");
// ignore: non_constant_identifier_names
final SelectProfile = selectProfileC.asFunction<GetJsonBlobStringFn>();
final ut8Onion = onion.toNativeUtf8();
SelectProfile(ut8Onion, ut8Onion.length);
}
// ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass) {
var createProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_CreateProfile");
@ -194,6 +269,8 @@ class CwtchFfi implements Cwtch {
final utf8nick = nick.toNativeUtf8();
final ut8pass = pass.toNativeUtf8();
CreateProfile(utf8nick, utf8nick.length, ut8pass, ut8pass.length);
malloc.free(utf8nick);
malloc.free(ut8pass);
}
// ignore: non_constant_identifier_names
@ -203,17 +280,19 @@ class CwtchFfi implements Cwtch {
final LoadProfiles = loadProfileC.asFunction<StringFn>();
final ut8pass = pass.toNativeUtf8();
LoadProfiles(ut8pass, ut8pass.length);
malloc.free(ut8pass);
}
// ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_function>>("c_GetMessage");
Future<String> GetMessage(String profile, int handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_GetMessage");
// ignore: non_constant_identifier_names
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrStrIntFn>();
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index);
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, handle, index);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
return jsonMessage;
}
@ -226,6 +305,8 @@ class CwtchFfi implements Cwtch {
final utf8onion = onion.toNativeUtf8();
final utf8json = json.toNativeUtf8();
SendAppBusEvent(utf8onion, utf8onion.length, utf8json, utf8json.length);
malloc.free(utf8onion);
malloc.free(utf8json);
}
@override
@ -236,52 +317,120 @@ class CwtchFfi implements Cwtch {
final SendAppBusEvent = sendAppBusEvent.asFunction<StringFn>();
final utf8json = json.toNativeUtf8();
SendAppBusEvent(utf8json, utf8json.length);
malloc.free(utf8json);
}
@override
// ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle) {
var acceptContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_AcceptContact");
void AcceptContact(String profileOnion, int contactHandle) {
var acceptContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_AcceptConversation");
// ignore: non_constant_identifier_names
final AcceptContact = acceptContact.asFunction<VoidFromStringStringFn>();
final AcceptContact = acceptContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
AcceptContact(u1, u1.length, u2, u2.length);
AcceptContact(u1, u1.length, contactHandle);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle) {
var blockContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_BlockContact");
void BlockContact(String profileOnion, int contactHandle) {
var blockContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_BlockContact");
// ignore: non_constant_identifier_names
final BlockContact = blockContact.asFunction<VoidFromStringStringFn>();
final BlockContact = blockContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
BlockContact(u1, u1.length, u2, u2.length);
BlockContact(u1, u1.length, contactHandle);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) {
var sendMessage = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SendMessage");
void SendMessage(String profileOnion, int contactHandle, String message) {
var sendMessage = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_SendMessage");
// ignore: non_constant_identifier_names
final SendMessage = sendMessage.asFunction<VoidFromStringStringStringFn>();
final SendMessage = sendMessage.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = message.toNativeUtf8();
SendMessage(u1, u1.length, u2, u2.length, u3, u3.length);
SendMessage(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1);
malloc.free(u3);
}
@override
// ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, String contactHandle, String target) {
var sendInvitation = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SendInvitation");
void SendInvitation(String profileOnion, int contactHandle, int target) {
var sendInvitation = library.lookup<NativeFunction<void_from_string_int_int_function>>("c_SendInvitation");
// ignore: non_constant_identifier_names
final SendInvitation = sendInvitation.asFunction<VoidFromStringStringStringFn>();
final SendInvitation = sendInvitation.asFunction<VoidFromStringIntIntFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = target.toNativeUtf8();
SendInvitation(u1, u1.length, u2, u2.length, u3, u3.length);
SendInvitation(u1, u1.length, contactHandle, target);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void ShareFile(String profileOnion, int contactHandle, String filepath) {
var shareFile = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_ShareFile");
// ignore: non_constant_identifier_names
final ShareFile = shareFile.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u3 = filepath.toNativeUtf8();
ShareFile(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1);
malloc.free(u3);
}
@override
// ignore: non_constant_identifier_names
void DownloadFile(String profileOnion, int contactHandle, String filepath, String manifestpath, String filekey) {
var dlFile = library.lookup<NativeFunction<void_from_string_int_string_string_string_function>>("c_DownloadFile");
// ignore: non_constant_identifier_names
final DownloadFile = dlFile.asFunction<VoidFromStringIntStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u3 = filepath.toNativeUtf8();
final u4 = manifestpath.toNativeUtf8();
final u5 = filekey.toNativeUtf8();
DownloadFile(u1, u1.length, contactHandle, u3, u3.length, u4, u4.length, u5, u5.length);
malloc.free(u1);
malloc.free(u3);
malloc.free(u4);
malloc.free(u5);
}
@override
// ignore: non_constant_identifier_names
void CreateDownloadableFile(String profileOnion, int contactHandle, String filenameSuggestion, String filekey) {
// android only - do nothing
}
// ignore: non_constant_identifier_names
void ExportPreviewedFile(String sourceFile, String suggestion) {
// android only - do nothing
}
@override
// ignore: non_constant_identifier_names
void CheckDownloadStatus(String profileOnion, String fileKey) {
var checkDownloadStatus = library.lookup<NativeFunction<string_string_to_void_function>>("c_CheckDownloadStatus");
// ignore: non_constant_identifier_names
final CheckDownloadStatus = checkDownloadStatus.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = fileKey.toNativeUtf8();
CheckDownloadStatus(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profileOnion, int contactHandle, String filekey) {
var fn = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_VerifyOrResumeDownload");
// ignore: non_constant_identifier_names
final VerifyOrResumeDownload = fn.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u3 = filekey.toNativeUtf8();
VerifyOrResumeDownload(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1);
malloc.free(u3);
}
@override
@ -302,33 +451,12 @@ class CwtchFfi implements Cwtch {
final u1 = profileOnion.toNativeUtf8();
final u2 = bundle.toNativeUtf8();
ImportBundle(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) {
var setGroupAttribute = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_SetGroupAttribute");
// ignore: non_constant_identifier_names
final SetGroupAttribute = setGroupAttribute.asFunction<VoidFromStringStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = value.toNativeUtf8();
SetGroupAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length);
}
@override
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle) {
var rejectInvite = library.lookup<NativeFunction<string_string_to_void_function>>("c_RejectInvite");
// ignore: non_constant_identifier_names
final RejectInvite = rejectInvite.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length);
}
@override
void CreateGroup(String profileOnion, String server, String groupName) {
var createGroup = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_CreateGroup");
// ignore: non_constant_identifier_names
@ -337,38 +465,32 @@ class CwtchFfi implements Cwtch {
final u2 = server.toNativeUtf8();
final u3 = groupName.toNativeUtf8();
CreateGroup(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
}
@override
// ignore: non_constant_identifier_names
void LeaveConversation(String profileOnion, String handle) {
var leaveConversation = library.lookup<NativeFunction<string_string_to_void_function>>("c_LeaveConversation");
void ArchiveConversation(String profileOnion, int handle) {
var archiveConversation = library.lookup<NativeFunction<string_int_to_void_function>>("c_ArchiveConversation");
// ignore: non_constant_identifier_names
final LeaveConversation = leaveConversation.asFunction<VoidFromStringStringFn>();
final ArchiveConversation = archiveConversation.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = handle.toNativeUtf8();
LeaveConversation(u1, u1.length, u2, u2.length);
ArchiveConversation(u1, u1.length, handle);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void LeaveGroup(String profileOnion, String groupHandle) {
var leaveGroup = library.lookup<NativeFunction<string_string_to_void_function>>("c_LeaveGroup");
void DeleteContact(String profileOnion, int handle) {
var deleteContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_DeleteContact");
// ignore: non_constant_identifier_names
final LeaveGroup = leaveGroup.asFunction<VoidFromStringStringFn>();
final DeleteContact = deleteContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
LeaveGroup(u1, u1.length, u2, u2.length);
}
@override
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
var updateMessageFlagsC = library.lookup<NativeFunction<void_from_string_string_int_int_function>>("c_UpdateMessageFlags");
// ignore: non_constant_identifier_names
final updateMessageFlags = updateMessageFlagsC.asFunction<VoidFromStringStringIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags);
DeleteContact(u1, u1.length, handle);
malloc.free(u1);
}
@override
@ -380,14 +502,164 @@ class CwtchFfi implements Cwtch {
final u1 = onion.toNativeUtf8();
final u2 = currentPassword.toNativeUtf8();
DeleteProfile(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void SetProfileAttribute(String profile, String key, String val) {
var setProfileAttribute = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SetProfileAttribute");
// ignore: non_constant_identifier_names
final SetProfileAttribute = setProfileAttribute.asFunction<VoidFromStringStringStringFn>();
final u1 = profile.toNativeUtf8();
final u2 = key.toNativeUtf8();
final u3 = val.toNativeUtf8();
SetProfileAttribute(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
}
@override
// ignore: non_constant_identifier_names
void SetConversationAttribute(String profile, int contact, String key, String val) {
var setContactAttribute = library.lookup<NativeFunction<void_from_string_int_string_string_function>>("c_SetConversationAttribute");
// ignore: non_constant_identifier_names
final SetContactAttribute = setContactAttribute.asFunction<VoidFromStringIntStringStringFn>();
final u1 = profile.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = val.toNativeUtf8();
SetContactAttribute(u1, u1.length, contact, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u3);
malloc.free(u4);
}
@override
// ignore: non_constant_identifier_names
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val) {
var setMessageAttribute = library.lookup<NativeFunction<void_from_string_int_int_int_string_string_function>>("c_SetMessageAttribute");
// ignore: non_constant_identifier_names
final SetMessageAttribute = setMessageAttribute.asFunction<VoidFromStringIntIntIntStringStringFn>();
final u1 = profile.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = val.toNativeUtf8();
SetMessageAttribute(u1, u1.length, conversation, channel, message, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u3);
malloc.free(u4);
}
@override
// ignore: non_constant_identifier_names
void LoadServers(String password) {
var loadServers = library.lookup<NativeFunction<string_to_void_function>>("c_LoadServers");
// ignore: non_constant_identifier_names
final LoadServers = loadServers.asFunction<StringFn>();
final u1 = password.toNativeUtf8();
LoadServers(u1, u1.length);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void CreateServer(String password, String description, bool autostart) {
var createServer = library.lookup<NativeFunction<void_from_string_string_byte_function>>("c_CreateServer");
// ignore: non_constant_identifier_names
final CreateServer = createServer.asFunction<VoidFromStringStringByteFn>();
final u1 = password.toNativeUtf8();
final u2 = description.toNativeUtf8();
CreateServer(u1, u1.length, u2, u2.length, autostart ? 1 : 0);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void DeleteServer(String serverOnion, String password) {
var deleteServer = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteServer");
// ignore: non_constant_identifier_names
final DeleteServer = deleteServer.asFunction<VoidFromStringStringFn>();
final u1 = serverOnion.toNativeUtf8();
final u2 = password.toNativeUtf8();
DeleteServer(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void LaunchServers() {
var launchServers = library.lookup<NativeFunction<Void Function()>>("c_LaunchServers");
// ignore: non_constant_identifier_names
final LaunchServers = launchServers.asFunction<void Function()>();
LaunchServers();
}
@override
// ignore: non_constant_identifier_names
void LaunchServer(String serverOnion) {
var launchServer = library.lookup<NativeFunction<string_to_void_function>>("c_LaunchServer");
// ignore: non_constant_identifier_names
final LaunchServer = launchServer.asFunction<StringFn>();
final u1 = serverOnion.toNativeUtf8();
LaunchServer(u1, u1.length);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void StopServer(String serverOnion) {
var shutdownServer = library.lookup<NativeFunction<string_to_void_function>>("c_StopServer");
// ignore: non_constant_identifier_names
final ShutdownServer = shutdownServer.asFunction<StringFn>();
final u1 = serverOnion.toNativeUtf8();
ShutdownServer(u1, u1.length);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void StopServers() {
var shutdownServers = library.lookup<NativeFunction<Void Function()>>("c_StopServers");
// ignore: non_constant_identifier_names
final ShutdownServers = shutdownServers.asFunction<void Function()>();
ShutdownServers();
}
@override
// ignore: non_constant_identifier_names
void DestroyServers() {
var destroyServers = library.lookup<NativeFunction<Void Function()>>("c_DestroyServers");
// ignore: non_constant_identifier_names
final DestroyServers = destroyServers.asFunction<void Function()>();
DestroyServers();
}
@override
// ignore: non_constant_identifier_names
void SetServerAttribute(String serverOnion, String key, String val) {
var setServerAttribute = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SetServerAttribute");
// ignore: non_constant_identifier_names
final SetServerAttribute = setServerAttribute.asFunction<VoidFromStringStringStringFn>();
final u1 = serverOnion.toNativeUtf8();
final u2 = key.toNativeUtf8();
final u3 = val.toNativeUtf8();
SetServerAttribute(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
}
@override
// ignore: non_constant_identifier_names
Future<void> Shutdown() async {
var shutdown = library.lookup<NativeFunction<void_from_void_funtion>>("c_ShutdownCwtch");
// ignore: non_constant_identifier_names
// Shutdown Cwtch + Tor...
// ignore: non_constant_identifier_names
final Shutdown = shutdown.asFunction<VoidFromVoidFunction>();
Shutdown();
@ -400,15 +672,65 @@ class CwtchFfi implements Cwtch {
}
@override
Future GetMessageByContentHash(String profile, String handle, String contentHash) async {
var getMessagesByContentHashC = library.lookup<NativeFunction<get_json_blob_from_str_str_str_function>>("c_GetMessagesByContentHash");
// ignore: non_constant_identifier_names
Future GetMessageByContentHash(String profile, int handle, String contentHash) async {
var getMessagesByContentHashC = library.lookup<NativeFunction<get_json_blob_from_str_int_string_function>>("c_GetMessagesByContentHash");
// ignore: non_constant_identifier_names
final GetMessagesByContentHash = getMessagesByContentHashC.asFunction<GetJsonBlobFromStrStrStrFn>();
final GetMessagesByContentHash = getMessagesByContentHashC.asFunction<GetJsonBlobFromStrIntStringFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
final utf8contentHash = contentHash.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length);
Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, handle, utf8contentHash, utf8contentHash.length);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
malloc.free(utf8contentHash);
return jsonMessage;
}
// ignore: non_constant_identifier_names
// Incredibly dangerous function which invokes a free in libCwtch, should only be used
// as documented in `MEMORY.md` in libCwtch repo.
void _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(Pointer<Utf8> ptr) {
var free = library.lookup<NativeFunction<free_function>>("c_FreePointer");
final Free = free.asFunction<FreeFn>();
Free(ptr);
}
@override
String defaultDownloadPath() {
Map<String, String> envVars = Platform.environment;
return path.join(envVars[Platform.isWindows ? 'UserProfile' : 'HOME']!, "Downloads");
}
@override
// ignore: non_constant_identifier_names
Future<String> GetMessageByID(String profile, int handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_GetMessageByID");
// ignore: non_constant_identifier_names
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, handle, index);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
return jsonMessage;
}
@override
// ignore: non_constant_identifier_names
void ChangePassword(String profile, String pass, String newpass, String newpassAgain) {
var changePasswordC = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_ChangePassword");
// ignore: non_constant_identifier_names
final ChangePasswordFn = changePasswordC.asFunction<VoidFromStringStringStringStringFn>();
final utf8profile = profile.toNativeUtf8();
final utf8pass = pass.toNativeUtf8();
final utf8newpass = newpass.toNativeUtf8();
final utf8newpasssagain = newpassAgain.toNativeUtf8();
ChangePasswordFn(utf8profile, utf8profile.length, utf8pass, utf8pass.length, utf8newpass, utf8newpass.length, utf8newpasssagain, utf8newpasssagain.length);
malloc.free(utf8profile);
malloc.free(utf8pass);
malloc.free(utf8newpass);
malloc.free(utf8newpasssagain);
}
}

View File

@ -28,6 +28,7 @@ class CwtchGomobile implements Cwtch {
late Future<dynamic> androidLibraryDir;
late Future<dynamic> androidHomeDirectory;
String androidHomeDirectoryStr = "";
late CwtchNotifier cwtchNotifier;
CwtchGomobile(CwtchNotifier _cwtchNotifier) {
@ -44,7 +45,8 @@ class CwtchGomobile implements Cwtch {
// ignore: non_constant_identifier_names
Future<void> Start() async {
print("gomobile.dart: Start()...");
var cwtchDir = path.join((await androidHomeDirectory).path, ".cwtch");
androidHomeDirectoryStr = (await androidHomeDirectory).path;
var cwtchDir = path.join(androidHomeDirectoryStr, ".cwtch");
if (EnvironmentConfig.BUILD_VER == dev_version) {
cwtchDir = path.join(cwtchDir, "dev");
}
@ -66,11 +68,6 @@ class CwtchGomobile implements Cwtch {
cwtchNotifier.handleMessage(call.method, obj);
}
// ignore: non_constant_identifier_names
void SelectProfile(String onion) {
cwtchPlatform.invokeMethod("SelectProfile", {"profile": onion});
}
// ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass) {
cwtchPlatform.invokeMethod("CreateProfile", {"nick": nick, "pass": pass});
@ -87,9 +84,13 @@ class CwtchGomobile implements Cwtch {
}
// ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index) {
print("gomobile.dart GetMessage " + index.toString());
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index});
Future<dynamic> GetMessage(String profile, int conversation, int index) {
return cwtchPlatform.invokeMethod("GetMessage", {"ProfileOnion": profile, "conversation": conversation, "index": index});
}
// ignore: non_constant_identifier_names
Future<dynamic> GetMessageByID(String profile, int conversation, int id) {
return cwtchPlatform.invokeMethod("GetMessageByID", {"ProfileOnion": profile, "conversation": conversation, "id": id});
}
@override
@ -109,26 +110,63 @@ class CwtchGomobile implements Cwtch {
@override
// ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "handle": contactHandle});
void AcceptContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "conversation": conversation});
}
@override
// ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "handle": contactHandle});
void BlockContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "conversation": conversation});
}
@override
// ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) {
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "handle": contactHandle, "message": message});
void SendMessage(String profileOnion, int conversation, String message) {
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
}
@override
// ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, String contactHandle, String target) {
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target});
void SendInvitation(String profileOnion, int conversation, int target) {
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
}
@override
// ignore: non_constant_identifier_names
void ShareFile(String profileOnion, int conversation, String filepath) {
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
}
@override
// ignore: non_constant_identifier_names
void DownloadFile(String profileOnion, int conversation, String filepath, String manifestpath, String filekey) {
cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey});
}
// ignore: non_constant_identifier_names
void CreateDownloadableFile(String profileOnion, int conversation, String filenameSuggestion, String filekey) {
cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filename": filenameSuggestion, "filekey": filekey});
}
// ignore: non_constant_identifier_names
void ExportPreviewedFile(String sourceFile, String suggestion) {
cwtchPlatform.invokeMethod("ExportPreviewedFile", {
"Path": sourceFile,
"FileName": suggestion,
});
}
@override
// ignore: non_constant_identifier_names
void CheckDownloadStatus(String profileOnion, String fileKey) {
cwtchPlatform.invokeMethod("CheckDownloadStatus", {"ProfileOnion": profileOnion, "fileKey": fileKey});
}
@override
// ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profileOnion, int conversation, String filekey) {
cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "conversation": conversation, "filekey": filekey});
}
@override
@ -143,18 +181,6 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("ImportBundle", {"ProfileOnion": profileOnion, "bundle": bundle});
}
@override
// ignore: non_constant_identifier_names
void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) {
cwtchPlatform.invokeMethod("SetGroupAttribute", {"ProfileOnion": profileOnion, "groupHandle": groupHandle, "key": key, "value": value});
}
@override
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "groupHandle": groupHandle});
}
@override
void CreateGroup(String profileOnion, String server, String groupName) {
cwtchPlatform.invokeMethod("CreateGroup", {"ProfileOnion": profileOnion, "server": server, "groupName": groupName});
@ -162,20 +188,80 @@ class CwtchGomobile implements Cwtch {
@override
// ignore: non_constant_identifier_names
void LeaveGroup(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "groupHandle": groupHandle});
void DeleteContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "conversation": conversation});
}
@override
// ignore: non_constant_identifier_names
void LeaveConversation(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("LeaveConversation", {"ProfileOnion": profileOnion, "contactHandle": contactHandle});
void ArchiveConversation(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "conversation": conversation});
}
@override
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
print("gomobile.dart UpdateMessageFlags " + index.toString());
cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "index": index, "flags": flags});
// ignore: non_constant_identifier_names
void SetProfileAttribute(String profile, String key, String val) {
cwtchPlatform.invokeMethod("SetProfileAttribute", {"ProfileOnion": profile, "Key": key, "Val": val});
}
@override
// ignore: non_constant_identifier_names
void SetConversationAttribute(String profile, int conversation, String key, String val) {
cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "conversation": conversation, "Key": key, "Val": val});
}
@override
// ignore: non_constant_identifier_names
void LoadServers(String password) {
cwtchPlatform.invokeMethod("LoadServers", {"Password": password});
}
@override
// ignore: non_constant_identifier_names
void CreateServer(String password, String description, bool autostart) {
cwtchPlatform.invokeMethod("CreateServer", {"Password": password, "Description": description, "Autostart": autostart});
}
@override
// ignore: non_constant_identifier_names
void DeleteServer(String serverOnion, String password) {
cwtchPlatform.invokeMethod("DeleteServer", {"ServerOnion": serverOnion, "Password": password});
}
@override
// ignore: non_constant_identifier_names
void LaunchServers() {
cwtchPlatform.invokeMethod("LaunchServers", {});
}
@override
// ignore: non_constant_identifier_names
void LaunchServer(String serverOnion) {
cwtchPlatform.invokeMethod("LaunchServer", {"ServerOnion": serverOnion});
}
@override
// ignore: non_constant_identifier_names
void StopServer(String serverOnion) {
cwtchPlatform.invokeMethod("StopServer", {"ServerOnion": serverOnion});
}
@override
// ignore: non_constant_identifier_names
void StopServers() {
cwtchPlatform.invokeMethod("StopServers", {});
}
@override
// ignore: non_constant_identifier_names
void DestroyServers() {
cwtchPlatform.invokeMethod("DestroyServers", {});
}
@override
// ignore: non_constant_identifier_names
void SetServerAttribute(String serverOnion, String key, String val) {
cwtchPlatform.invokeMethod("SetServerAttribute", {"ServerOnion": serverOnion, "Key": key, "Val": val});
}
@override
@ -185,7 +271,22 @@ class CwtchGomobile implements Cwtch {
}
@override
Future GetMessageByContentHash(String profile, String handle, String contentHash) {
return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"profile": profile, "contact": handle, "contentHash": contentHash});
Future GetMessageByContentHash(String profile, int conversation, String contentHash) {
return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"ProfileOnion": profile, "conversation": conversation, "contentHash": contentHash});
}
@override
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val) {
cwtchPlatform.invokeMethod("SetMessageAttribute", {"ProfileOnion": profile, "conversation": conversation, "Channel": channel, "Message": message, "Key": key, "Val": val});
}
@override
String defaultDownloadPath() {
return this.androidHomeDirectoryStr;
}
@override
void ChangePassword(String profile, String pass, String newpass, String newpassAgain) {
cwtchPlatform.invokeMethod("ChangePassword", {"ProfileOnion": profile, "OldPass": pass, "NewPass": newpass, "NewPassAgain": newpassAgain});
}
}

View File

@ -104,6 +104,16 @@ class CwtchIcons {
static const IconData add_24px = IconData(0xe850, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData address_copy_2 = IconData(0xe852, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData address = IconData(0xe856, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData streamer_bunnymask = IconData(0xe85b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData streamer_ghost = IconData(0xe85c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData cancel_schedule_send_black_24dp = IconData(0xe85d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData more_horiz_black_24dp = IconData(0xe85e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData dns_black_add_24dp = IconData(0xe85f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData dns_black_24dp = IconData(0xe860, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData info_black_24dp = IconData(0xe861, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData accept_unknown = IconData(0xe862, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData eye_closed_1 = IconData(0xe863, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData eye_open_1 = IconData(0xe864, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData send_invite = IconData(0xe888, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData leave_group = IconData(0xe88a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData leave_chat = IconData(0xe88b, fontFamily: _kFontFam, fontPackage: _kFontPkg);

View File

@ -6,12 +6,17 @@ class ErrorHandler extends ChangeNotifier {
// Add Contact Specific Errors...
static const String addContactErrorPrefix = "addcontact";
static const String changePasswordErrorPrefix = "changepassword";
static const String invalidImportStringErrorType = "invalid_import_string";
static const String contactAlreadyExistsErrorType = "contact_already_exists";
bool invalidImportStringError = false;
bool contactAlreadyExistsError = false;
bool explicitAddContactSuccess = false;
// ChangePassword
bool changePasswordError = false;
bool explicitChangePasswordSuccess = false;
// Import Bundle Specific Errors
static const String importBundleErrorPrefix = "importBundle";
bool importBundleError = false;
@ -21,6 +26,30 @@ class ErrorHandler extends ChangeNotifier {
bool deleteProfileError = false;
bool deleteProfileSuccess = false;
static const String deletedServerErrorPrefix = "deletedserver";
bool deletedServerError = false;
bool deletedServerSuccess = false;
reset() {
invalidImportStringError = false;
contactAlreadyExistsError = false;
explicitAddContactSuccess = false;
importBundleError = false;
importBundleSuccess = false;
deleteProfileError = false;
deleteProfileSuccess = false;
deletedServerError = false;
deletedServerSuccess = false;
changePasswordError = false;
explicitChangePasswordSuccess = false;
notifyListeners();
}
/// Called by the event bus.
handleUpdate(String error) {
var parts = error.split(".");
@ -37,6 +66,11 @@ class ErrorHandler extends ChangeNotifier {
case deleteProfileErrorPrefix:
handleDeleteProfileError(errorType);
break;
case changePasswordErrorPrefix:
handleChangePasswordError(errorType);
break;
case deletedServerErrorPrefix:
handleDeletedServerError(errorType);
}
notifyListeners();
@ -91,4 +125,34 @@ class ErrorHandler extends ChangeNotifier {
break;
}
}
handleChangePasswordError(String errorType) {
// Reset add contact errors
changePasswordError = false;
explicitChangePasswordSuccess = false;
switch (errorType) {
case successErrorType:
explicitChangePasswordSuccess = true;
break;
default:
changePasswordError = true;
break;
}
}
handleDeletedServerError(String errorType) {
// reset
deletedServerError = false;
deletedServerSuccess = false;
switch (errorType) {
case successErrorType:
deletedServerSuccess = true;
break;
default:
deletedServerError = true;
break;
}
}
}

View File

@ -1,6 +1,94 @@
{
"@@locale": "de",
"@@last_modified": "2021-07-07T23:42:20+02:00",
"@@last_modified": "2021-12-20T09:20:03+01:00",
"msgAddToAccept": "Add this account to your contacts in order to accept this file.",
"btnSendFile": "Send File",
"msgConfirmSend": "Are you sure you want to send",
"msgFileTooBig": "File size cannot exceed 10 GB",
"storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...",
"loadingCwtch": "Loading Cwtch...",
"themeColorLabel": "Color Theme",
"themeNameNeon2": "Neon2",
"themeNameNeon1": "Neon1",
"themeNameMidnight": "Midnight",
"themeNameMermaid": "Mermaid",
"themeNamePumpkin": "Pumpkin",
"themeNameGhost": "Ghost",
"themeNameVampire": "Vampire",
"themeNameWitch": "Witch",
"themeNameCwtch": "Cwtch",
"settingDownloadFolder": "Download Folder",
"settingImagePreviewsDescription": "Images will be downloaded and previewed automatically. Please note that image previews can often lead to security vulnerabilities, and you should not enable this Experiment if you use Cwtch with untrusted contacts. Profile pictures are planned for Cwtch 1.6.",
"settingImagePreviews": "Image Previews and Profile Pictures",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"newMessagesLabel": "New Messages",
"localeRU": "Russian",
"copyServerKeys": "Copy keys",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
"deleteServerConfirmBtn": "Really delete server",
"deleteServerSuccess": "Successfully deleted server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"downloadFileButton": "Herunterladen",
"titleManageProfilesShort": "Profiles",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
"messageFileOffered": "Contact is offering to send you a file",
"messageFileSent": "You sent a file",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size",
"labelFilename": "Filename",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
"plainProfileDescription": "We recommend that you protect your Cwtch profiles with a password. If you do not set a password on this profile then anyone who has access to this device may be able to access information about this profile, including contacts, messages and sensitive cryptographic keys.",
"encryptedProfileDescription": "Encrypting a profile with a password protects it from other people who may also use this device. Encrypted profiles cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
@ -12,17 +100,31 @@
"tooltipReplyToThisMessage": "Reply to this message",
"tooltipRejectContactRequest": "Reject this contact request",
"tooltipAcceptContactRequest": "Accept this contact request.",
"experimentsEnabled": "Experimente aktiviert",
"malformedMessage": "Fehlerhafte Nachricht",
"contactSuggestion": "Dieser Kontaktvorschlag ist für: ",
"descriptionBlockUnknownConnections": "Falls aktiviert, wird diese Einstellung alle Verbindungen von Cwtch Usern autmoatisch schliessen, wenn sie nicht in deinen Kontakten sind.",
"descriptionExperimentsGroups": "Mit experimentellen Gruppen kann Cwtch über nicht vertrauenswürdige Serverinfrastruktur die Kommunikation mit mehr als einem Kontakt vereinfachen.",
"descriptionExperiments": "Experimentelle Cwtch Features sind optionale, opt-in Features für die andere Privatsphärenaspekte berücksichtigt werden als bei traditionellen 1:1 metadatenresistenten Chats, wie z. B. Gruppennachrichten, Bots usw.",
"networkStatusDisconnected": "Vom Internet getrennt, überprüfe deine Verbindung",
"yourServers": "Deine Server",
"yourProfiles": "Deine Profile",
"enterProfilePassword": "Gib ein Passwort ein, um deine Profile anzuzeigen",
"deleteConfirmLabel": "Gib LÖSCHEN ein um zu bestätigen",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"cycleColoursAndroid": "Klicken um Farbe zu wechseln.\nGedrückt halten zum zurücksetzen.",
"cycleMorphsDesktop": "Klicken um Morph zu wechseln.\nRechtsklick zum zurücksetzen.",
"cycleMorphsAndroid": "Klicken um Morph zu wechseln.\nGedrückt halten zum zurücksetzen.",
"pasteAddressToAddContact": "Adresse, Einladung oder Schlüssel hier hinzufügen, um einen Kontakt hinzuzufügen",
"notificationNewMessageFromGroup": "Neue Nachricht in einer Gruppe!",
"notificationNewMessageFromPeer": "Neue Nachricht von einem Kontakt!",
"tooltipHidePassword": "Password verstecken",
"tooltipShowPassword": "Password anzeigen",
"serverNotSynced": "Neue Nachrichten abrufen (Dies kann eine Weile dauern...)",
"groupInviteSettingsWarning": "Du wurdest eingeladen einer Gruppe beizutreten! Bitte aktiviere das Gruppenchat Experiment in den Einstellungen um diese Einladung anzusehen.",
"shutdownCwtchAction": "Cwtch schliessen",
"shutdownCwtchDialog": "Bist du sicher, dass du Cwtch schliessen möchtest? Alle Verbindungen werden geschlossen und die App wird beendet.",
"shutdownCwtchDialogTitle": "Cwtch schliessen?",
"shutdownCwtchTooltip": "Cwtch schliessen",
"malformedMessage": "Fehlerhafte Nachricht",
"profileDeleteSuccess": "Profil erfolgreich gelöscht",
"debugLog": "Konsolendebuglogging aktivivieren",
"torNetworkStatus": "Tor Netzwerkstatus",
@ -38,7 +140,6 @@
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "Du hast eine Einladung geschickt für: ",
"contactSuggestion": "Dieser Kontaktvorschlag ist für: ",
"rejected": "Abgelehnt!",
"accepted": "Angenommen!",
"chatHistoryDefault": "Diese Unterhaltung wird gelöscht sobald Cwtch geschlossen wird! Der Nachrichtenverlauf für jede Unterhaltung kann im Einstellungsmenü oben rechts geändert werden.",
@ -47,9 +148,6 @@
"reallyLeaveThisGroupPrompt": "Bist du sicher, dass du diese Unterhaltung beenden möchtest? Alle Nachrichten und Attribute werden gelöscht.",
"leaveGroup": "Unterhaltung beenden",
"inviteToGroup": "Du wurdest eingeladen einer Gruppe beizutreten:",
"pasteAddressToAddContact": "Adresse, Einladung oder Schlüssel hier hinzufügen, um einen Kontakt hinzuzufügen",
"tooltipAddContact": "Neuen Kontakt oder Unterhaltung hinzufügen",
"titleManageContacts": "Unterhaltungen",
"titleManageServers": "Server verwalten",
"dateNever": "Nie",
"dateLastYear": "Letzes Jahr",
@ -57,89 +155,110 @@
"dateLastMonth": "Letzter Monat",
"dateRightNow": "Jetzt",
"successfullAddedContact": "Erfolgreich hinzugefügt",
"descriptionBlockUnknownConnections": "Falls aktiviert, wird diese Einstellung alle Verbindungen von Cwtch Usern autmoatisch schliessen, wenn sie nicht in deinen Kontakten sind.",
"descriptionExperimentsGroups": "Mit experimentellen Gruppen kann Cwtch über nicht vertrauenswürdige Serverinfrastruktur die Kommunikation mit mehr als einem Kontakt vereinfachen.",
"descriptionExperiments": "Experimentelle Cwtch Features sind optionale, opt-in Features für die andere Privatsphärenaspekte berücksichtigt werden als bei traditionellen 1:1 metadatenresistenten Chats, wie z. B. Gruppennachrichten, Bots usw.",
"titleManageProfiles": "Cwtch Profile verwalten",
"tooltipUnlockProfiles": "Entsperre verschlüsselte Profile durch Eingabe des Passworts.",
"titleManageContacts": "Unterhaltungen",
"tooltipAddContact": "Neuen Kontakt oder Unterhaltung hinzufügen",
"tooltipOpenSettings": "Öfffne das Einstellungsmenü",
"invalidImportString": "Ungültiger Importstring",
"contactAlreadyExists": "Kontakt existiert bereits",
"invalidImportString": "Ungültiger Importstring",
"conversationSettings": "Unterhaltungseinstellungen",
"enterCurrentPasswordForDelete": "Bitte gib das aktuelle Passwort ein, um diese Profil zu löschen.",
"enableGroups": "Gruppenchat aktivieren",
"experimentsEnabled": "Experimente aktiviert",
"localeIt": "Italiana",
"localeEs": "Espanol",
"addListItem": "Liste hinzufügen",
"addNewItem": "Ein neues Element zur Liste hinzufügen",
"todoPlaceholder": "noch zu erledigen",
"newConnectionPaneTitle": "Neue Verbindung",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
"networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen",
"networkStatusDisconnected": "Vom Internet getrennt, überprüfe deine Verbindung",
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
"loadingTor": "Tor wird geladen...",
"smallTextLabel": "Klein",
"defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:",
"builddate": "Aufgebaut auf: %2",
"version": "Version %1",
"versionTor": "Version %1 mit tor %2",
"themeDark": "Dunkel",
"themeLight": "Licht",
"settingTheme": "Thema",
"largeTextLabel": "Groß",
"settingInterfaceZoom": "Zoomstufe",
"localeDe": "Deutsche",
"localePt": "Portuguesa",
"localeFr": "Frances",
"localeEn": "English",
"settingLanguage": "Sprache",
"blockUnknownLabel": "Unbekannte Peers blockieren",
"zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)",
"versionBuilddate": "Version: %1 Aufgebaut auf: %2",
"cwtchSettingsTitle": "Cwtch Einstellungen",
"unlock": "Entsperren",
"yourServers": "Deine Server",
"yourProfiles": "Deine Profile",
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
"password": "Passwort",
"enterProfilePassword": "Gib ein Passwort ein, um deine Profile anzuzeigen",
"addNewProfileBtn": "Neues Profil hinzufügen",
"deleteConfirmText": "LÖSCHEN",
"deleteProfileConfirmBtn": "Profil wirklich löschen",
"deleteConfirmLabel": "Gib LÖSCHEN ein um zu bestätigen",
"deleteProfileBtn": "Profil löschen",
"passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt",
"passwordErrorMatch": "Passwörter stimmen nicht überein",
"saveProfileBtn": "Profil speichern",
"createProfileBtn": "Profil speichern",
"passwordErrorEmpty": "Passwort darf nicht leer sein",
"password2Label": "Passwort erneut eingeben",
"password1Label": "Passwort",
"currentPasswordLabel": "aktuelles Passwort",
"yourDisplayName": "Dein Anzeigename",
"profileOnionLabel": "Sende diese Adresse an andere Nutzer, mit denen du dich verbinden möchtest",
"noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.",
"radioNoPassword": "Unverschlüsselt (kein Passwort)",
"radioUsePassword": "Passwort",
"copiedToClipboardNotification": "in die Zwischenablage kopiert",
"copyBtn": "Kopieren",
"editProfile": "Profil bearbeiten",
"newProfile": "Neues Profil",
"defaultProfileName": "Alice",
"profileName": "Anzeigename",
"editProfileTitle": "Profil bearbeiten",
"addProfileTitle": "Neues Profil hinzufügen",
"deleteBtn": "löschen",
"unblockBtn": "Anderen Nutzer entsperren",
"dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen",
"savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.",
"savePeerHistory": "Peer-Verlauf speichern",
"blockBtn": "Anderen Nutzer blockieren",
"saveBtn": "speichern",
"displayNameLabel": "Angezeigename",
"peerOfflineMessage": "Anderer Nutzer ist offline, Nachrichten können derzeit nicht zugestellt werden",
"peerBlockedMessage": "Anderer Nutzer ist blockiert",
"dmTooltip": "Klicken, um Direktnachricht zu senden",
"peerNotOnline": "Der andere Nutzer ist offline. Die App kann momentan nicht verwendet werden.",
"searchList": "Liste durchsuchen",
"update": "Update",
"viewServerInfo": "Serverinfo",
"serverNotSynced": "Neue Nachrichten abrufen (Dies kann eine Weile dauern...)",
"serverSynced": "synchronisiert",
"cycleColoursDesktop": "Klicken um Farbe zu wechseln.\nRechtsklick zum zurücksetzen.",
"cycleCatsDesktop": "Klicken um Kategorie zu wechseln.\nRechtslick zum zurücksetzen.",
"cycleCatsAndroid": "Klicken um Kategorie zu wechseln.\nLanger Klick zum zurücksetzen.",
"addPeer": "Anderen Nutzer hinzufügen",
"addPeerTab": "Einen anderen Nutzer hinzufügen",
"todoPlaceholder": "noch zu erledigen",
"addListItem": "Liste hinzufügen",
"addNewItem": "Ein neues Element zur Liste hinzufügen",
"createGroupTab": "Eine Gruppe erstellen",
"joinGroupTab": "Einer Gruppe beitreten",
"peerAddress": "Adresse",
"peerName": "Namen",
"groupName": "Gruppenname",
"server": "Server",
"invitation": "Einladung",
"groupAddr": "Adresse",
"createGroup": "Gruppe erstellen",
"joinGroup": "Gruppe beitreten",
"blocked": "Blockiert",
"search": "Suche...",
"serverInfo": "Server-Informationen",
"serverConnectivityConnected": "Server verbunden",
"serverConnectivityDisconnected": "Server getrennt",
"addListItemBtn": "Element hinzufügen",
"savePeerHistory": "Peer-Verlauf speichern",
"addProfileTitle": "Neues Profil hinzufügen",
"editProfileTitle": "Profil bearbeiten",
"profileName": "Anzeigename",
"defaultProfileName": "Alice",
"newProfile": "Neues Profil",
"editProfile": "Profil bearbeiten",
"radioUsePassword": "Passwort",
"radioNoPassword": "Unverschlüsselt (kein Passwort)",
"noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.",
"deleteProfileBtn": "Profil löschen",
"deleteConfirmText": "LÖSCHEN",
"deleteProfileConfirmBtn": "Profil wirklich löschen",
"addNewProfileBtn": "Neues Profil hinzufügen",
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
"newConnectionPaneTitle": "Neue Verbindung",
"password1Label": "Passwort",
"password2Label": "Passwort erneut eingeben",
"createProfileBtn": "Profil speichern",
"saveProfileBtn": "Profil speichern",
"passwordErrorMatch": "Passwörter stimmen nicht überein",
"passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt",
"password": "Passwort",
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
"unlock": "Entsperren",
"versionBuilddate": "Version: %1 Aufgebaut auf: %2",
"blockUnknownLabel": "Unbekannte Peers blockieren",
"settingLanguage": "Sprache",
"localeDe": "Deutsche",
"settingInterfaceZoom": "Zoomstufe",
"settingTheme": "Thema",
"themeLight": "Licht",
"themeDark": "Dunkel",
"versionTor": "Version %1 mit tor %2",
"version": "Version %1",
"builddate": "Aufgebaut auf: %2",
"loadingTor": "Tor wird geladen...",
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
"networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen",
"networkStatusOnline": "Online",
"smallTextLabel": "Klein",
"defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:",
"largeTextLabel": "Groß",
"zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)",
"cwtchSettingsTitle": "Cwtch Einstellungen",
"copiedToClipboardNotification": "in die Zwischenablage kopiert",
"deleteBtn": "Löschen",
"saveBtn": "Speichern",
"addressLabel": "Adresse",
"puzzleGameBtn": "Puzzlespiel",
"bulletinsBtn": "Meldungen",
@ -150,50 +269,19 @@
"acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen",
"newGroupBtn": "Neue Gruppe anlegen",
"copiedClipboardNotification": "in die Zwischenablage kopiert",
"peerOfflineMessage": "Anderer Nutzer ist offline, Nachrichten können derzeit nicht zugestellt werden",
"peerBlockedMessage": "Anderer Nutzer ist blockiert",
"copyBtn": "Kopieren",
"pendingLabel": "Bestätigung ausstehend",
"acknowledgedLabel": "bestätigt",
"couldNotSendMsgError": "Nachricht konnte nicht gesendet werden",
"dmTooltip": "Klicken, um Direktnachricht zu senden",
"membershipDescription": "Unten steht eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Möglicherweise enthält diese Benutzerzliste nicht alle, die Zugang zur Gruppe haben.",
"addListItemBtn": "Element hinzufügen",
"peerNotOnline": "Der andere Nutzer ist offline. Die App kann momentan nicht verwendet werden.",
"searchList": "Liste durchsuchen",
"update": "Update",
"inviteBtn": "Einladen",
"inviteToGroupLabel": "In die Gruppe einladen",
"groupNameLabel": "Gruppenname",
"viewServerInfo": "Serverinfo",
"serverSynced": "synchronisiert",
"serverConnectivityDisconnected": "Server getrennt",
"serverConnectivityConnected": "Server verbunden",
"serverInfo": "Server-Informationen",
"invitationLabel": "Einladung",
"serverLabel": "Server",
"search": "Suche...",
"cycleColoursDesktop": "Klicken um Farbe zu wechseln.\nRechtsklick zum zurücksetzen.",
"cycleColoursAndroid": "Klicken um Farbe zu wechseln.\nGedrückt halten zum zurücksetzen.",
"cycleMorphsDesktop": "Klicken um Morph zu wechseln.\nRechtsklick zum zurücksetzen.",
"cycleMorphsAndroid": "Klicken um Morph zu wechseln.\nGedrückt halten zum zurücksetzen.",
"cycleCatsDesktop": "Klicken um Kategorie zu wechseln.\nRechtslick zum zurücksetzen.",
"cycleCatsAndroid": "Klicken um Kategorie zu wechseln.\nLanger Klick zum zurücksetzen.",
"blocked": "Blockiert",
"titlePlaceholder": "Titel...",
"postNewBulletinLabel": "Neue Meldung veröffentlichen",
"newBulletinLabel": "Neue Meldung",
"joinGroup": "Gruppe beitreten",
"createGroup": "Gruppe erstellen",
"addPeer": "Anderen Nutzer hinzufügen",
"groupAddr": "Adresse",
"invitation": "Einladung",
"server": "Server",
"groupName": "Gruppenname",
"peerName": "Namen",
"peerAddress": "Adresse",
"joinGroupTab": "Einer Gruppe beitreten",
"createGroupTab": "Eine Gruppe erstellen",
"addPeerTab": "Einen anderen Nutzer hinzufügen",
"createGroupBtn": "Anlegen",
"defaultGroupName": "Tolle Gruppe",
"createGroupTitle": "Gruppe Anlegen"

View File

@ -1,6 +1,107 @@
{
"@@locale": "en",
"@@last_modified": "2021-07-07T23:42:20+02:00",
"@@last_modified": "2021-12-20T09:20:03+01:00",
"msgAddToAccept": "Add this account to your contacts in order to accept this file.",
"btnSendFile": "Send File",
"msgConfirmSend": "Are you sure you want to send",
"msgFileTooBig": "File size cannot exceed 10 GB",
"storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...",
"loadingCwtch": "Loading Cwtch...",
"themeColorLabel": "Color Theme",
"themeNameNeon2": "Neon2",
"themeNameNeon1": "Neon1",
"themeNameMidnight": "Midnight",
"themeNameMermaid": "Mermaid",
"themeNamePumpkin": "Pumpkin",
"themeNameGhost": "Ghost",
"themeNameVampire": "Vampire",
"themeNameWitch": "Witch",
"themeNameCwtch": "Cwtch",
"settingDownloadFolder": "Download Folder",
"settingImagePreviewsDescription": "Images will be downloaded and previewed automatically. Please note that image previews can often lead to security vulnerabilities, and you should not enable this Experiment if you use Cwtch with untrusted contacts. Profile pictures are planned for Cwtch 1.6.",
"settingImagePreviews": "Image Previews and Profile Pictures",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.",
"newMessagesLabel": "New Messages",
"localeRU": "Russian",
"copyServerKeys": "Copy keys",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"deleteServerConfirmBtn": "Really delete server",
"deleteServerSuccess": "Successfully deleted server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
"messageFileOffered": "Contact is offering to send you a file",
"messageFileSent": "You sent a file",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size",
"labelFilename": "Filename",
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Send this address to people you want to connect with",
"addPeerTab": "Add a contact",
"addPeer": "Add Contact",
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
"peerBlockedMessage": "Contact is blocked",
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
"blockBtn": "Block Contact",
"savePeerHistory": "Save History",
"dontSavePeerHistory": "Delete History",
"unblockBtn": "Unblock Contact",
"blockUnknownLabel": "Block Unknown Contacts",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"networkStatusConnecting": "Connecting to network and contacts...",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
"plainProfileDescription": "We recommend that you protect your Cwtch profiles with a password. If you do not set a password on this profile then anyone who has access to this device may be able to access information about this profile, including contacts, messages and sensitive cryptographic keys.",
"encryptedProfileDescription": "Encrypting a profile with a password protects it from other people who may also use this device. Encrypted profiles cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
@ -76,7 +177,6 @@
"todoPlaceholder": "Todo...",
"newConnectionPaneTitle": "New Connection",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Connecting to network and peers...",
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
"viewGroupMembershipTooltip": "View Group Membership",
@ -96,7 +196,6 @@
"localeFr": "Frances",
"localeEn": "English",
"settingLanguage": "Language",
"blockUnknownLabel": "Block Unknown Peers",
"zoomLabel": "Interface zoom (mostly affects text and button sizes)",
"versionBuilddate": "Version: %1 Built on: %2",
"cwtchSettingsTitle": "Cwtch Settings",
@ -120,12 +219,10 @@
"password1Label": "Password",
"currentPasswordLabel": "Current Password",
"yourDisplayName": "Your Display Name",
"profileOnionLabel": "Send this address to peers you want to connect with",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copied to Clipboard",
"copyBtn": "Copy",
"editProfile": "Edit Profille",
"newProfile": "New Profile",
"defaultProfileName": "Alice",
@ -133,11 +230,6 @@
"editProfileTitle": "Edit Profile",
"addProfileTitle": "Add new profile",
"deleteBtn": "Delete",
"unblockBtn": "Unblock Peer",
"dontSavePeerHistory": "Delete Peer History",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
"savePeerHistory": "Save Peer History",
"blockBtn": "Block Peer",
"saveBtn": "Save",
"displayNameLabel": "Display Name",
"addressLabel": "Address",
@ -150,20 +242,18 @@
"acceptGroupInviteLabel": "Do you want to accept the invitation to",
"newGroupBtn": "Create new group",
"copiedClipboardNotification": "Copied to clipboard",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
"peerBlockedMessage": "Peer is blocked",
"copyBtn": "Copy",
"pendingLabel": "Pending",
"acknowledgedLabel": "Acknowledged",
"couldNotSendMsgError": "Could not send this message",
"dmTooltip": "Click to DM",
"membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.",
"addListItemBtn": "Add Item",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Invite",
"inviteToGroupLabel": "Invite to group",
"groupNameLabel": "Group Name",
"groupNameLabel": "Group name",
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
@ -184,7 +274,6 @@
"newBulletinLabel": "New Bulletin",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
@ -193,7 +282,6 @@
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Create",
"defaultGroupName": "Awesome Group",
"createGroupTitle": "Create Group"

View File

@ -1,6 +1,94 @@
{
"@@locale": "es",
"@@last_modified": "2021-07-07T23:42:20+02:00",
"@@last_modified": "2021-12-20T09:20:03+01:00",
"msgAddToAccept": "Add this account to your contacts in order to accept this file.",
"btnSendFile": "Send File",
"msgConfirmSend": "Are you sure you want to send",
"msgFileTooBig": "File size cannot exceed 10 GB",
"storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...",
"loadingCwtch": "Loading Cwtch...",
"themeColorLabel": "Color Theme",
"themeNameNeon2": "Neon2",
"themeNameNeon1": "Neon1",
"themeNameMidnight": "Midnight",
"themeNameMermaid": "Mermaid",
"themeNamePumpkin": "Pumpkin",
"themeNameGhost": "Ghost",
"themeNameVampire": "Vampire",
"themeNameWitch": "Witch",
"themeNameCwtch": "Cwtch",
"settingDownloadFolder": "Download Folder",
"settingImagePreviewsDescription": "Images will be downloaded and previewed automatically. Please note that image previews can often lead to security vulnerabilities, and you should not enable this Experiment if you use Cwtch with untrusted contacts. Profile pictures are planned for Cwtch 1.6.",
"settingImagePreviews": "Image Previews and Profile Pictures",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"newMessagesLabel": "New Messages",
"localeRU": "Russian",
"copyServerKeys": "Copy keys",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
"deleteServerConfirmBtn": "Really delete server",
"deleteServerSuccess": "Successfully deleted server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
"messageFileOffered": "Contact is offering to send you a file",
"messageFileSent": "You sent a file",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size",
"labelFilename": "Filename",
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
"plainProfileDescription": "We recommend that you protect your Cwtch profiles with a password. If you do not set a password on this profile then anyone who has access to this device may be able to access information about this profile, including contacts, messages and sensitive cryptographic keys.",
"encryptedProfileDescription": "Encrypting a profile with a password protects it from other people who may also use this device. Encrypted profiles cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
@ -16,7 +104,6 @@
"notificationNewMessageFromPeer": "New message from a contact!",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Fuera de sincronización con el servidor",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
@ -47,9 +134,6 @@
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateNever": "Never",
"dateLastYear": "Last Year",
@ -62,139 +146,143 @@
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"titleManageContacts": "Conversations",
"tooltipAddContact": "Add a new contact or conversation",
"tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"invalidImportString": "Invalid import string",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Experimentos habilitados",
"localeIt": "Italiano",
"localeEs": "Español",
"addListItem": "Añadir un nuevo elemento a la lista",
"addNewItem": "Añadir un nuevo elemento a la lista",
"todoPlaceholder": "Por hacer...",
"newConnectionPaneTitle": "Nueva conexión",
"networkStatusOnline": "En línea",
"networkStatusConnecting": "Conectando a la red y a los contactos...",
"networkStatusAttemptingTor": "Intentando conectarse a la red Tor",
"networkStatusDisconnected": "Sin conexión, comprueba tu conexión",
"viewGroupMembershipTooltip": "Ver membresía del grupo",
"loadingTor": "Cargando tor...",
"smallTextLabel": "Pequeño",
"defaultScalingText": "Tamaño predeterminado de texto (factor de escala:",
"builddate": "Basado en: %2",
"version": "Versión %1",
"versionTor": "Versión %1 con tor %2",
"todoPlaceholder": "Por hacer...",
"bulletinsBtn": "Boletines",
"radioNoPassword": "Sin cifrado (sin contraseña)",
"themeDark": "Oscuro",
"themeLight": "Claro",
"settingTheme": "Tema",
"largeTextLabel": "Grande",
"settingInterfaceZoom": "Nivel de zoom",
"smallTextLabel": "Pequeño",
"loadingTor": "Cargando tor...",
"cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.",
"cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.",
"cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.",
"builddate": "Basado en: %2",
"cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"localeDe": "Alemán",
"localePt": "Portugués",
"localeFr": "Francés",
"localeEn": "Inglés",
"settingLanguage": "Idioma",
"blockUnknownLabel": "Bloquear conexiones desconocidas",
"zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)",
"versionBuilddate": "Versión: %1 Basado en %2",
"cwtchSettingsTitle": "Configuración de Cwtch",
"unlock": "Desbloquear",
"yourServers": "Tus servidores",
"yourProfiles": "Tus perfiles",
"error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña",
"password": "Contraseña",
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
"addNewProfileBtn": "Agregar nuevo perfil",
"deleteConfirmText": "ELIMINAR",
"deleteProfileConfirmBtn": "Confirmar eliminar perfil",
"deleteConfirmLabel": "Escribe ELIMINAR para confirmar",
"deleteProfileBtn": "Eliminar Perfil",
"passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada",
"passwordErrorMatch": "Las contraseñas no coinciden",
"saveProfileBtn": "Guardar perfil",
"createProfileBtn": "Crear perfil",
"passwordErrorEmpty": "El campo de contraseña no puede estar vacío",
"password2Label": "Vuelve a ingresar tu contraseña",
"password1Label": "Contraseña",
"currentPasswordLabel": "Contraseña actual",
"yourDisplayName": "Tu nombre de usuario",
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
"noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados",
"radioNoPassword": "Sin cifrado (sin contraseña)",
"radioUsePassword": "Contraseña",
"copiedToClipboardNotification": "Copiado al portapapeles",
"copyBtn": "Copiar",
"editProfile": "Editar perfil",
"newProfile": "Nuevo perfil",
"defaultProfileName": "Alicia",
"profileName": "Nombre de Usuario",
"editProfileTitle": "Editar perfil",
"addProfileTitle": "Agregar nuevo perfil",
"deleteBtn": "Eliminar",
"addListItem": "Añadir un nuevo elemento a la lista",
"unblockBtn": "Desbloquear contacto",
"dontSavePeerHistory": "Eliminar historial de contacto",
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
"savePeerHistory": "Guardar el historial con contacto",
"blockBtn": "Bloquear contacto",
"saveBtn": "Guardar",
"displayNameLabel": "Nombre de Usuario",
"addressLabel": "Dirección",
"puzzleGameBtn": "Juego de rompecabezas",
"bulletinsBtn": "Boletines",
"listsBtn": "Listas",
"chatBtn": "Chat",
"rejectGroupBtn": "Rechazar",
"acceptGroupBtn": "Aceptar",
"acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ",
"newGroupBtn": "Crear un nuevo grupo de chat",
"copiedClipboardNotification": "Copiado al portapapeles",
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
"peerBlockedMessage": "Contacto bloqueado",
"pendingLabel": "Pendiente",
"acknowledgedLabel": "Reconocido",
"couldNotSendMsgError": "No se pudo enviar este mensaje",
"dmTooltip": "Haz clic para enviar mensaje directo",
"membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo",
"addListItemBtn": "Agregar artículo",
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
"searchList": "Buscar en la lista",
"update": "Actualizar",
"inviteBtn": "Invitar",
"inviteToGroupLabel": "Invitar al grupo",
"groupNameLabel": "Nombre del grupo",
"viewServerInfo": "Información del servidor",
"serverSynced": "Sincronizado",
"serverConnectivityDisconnected": "Servidor desconectado",
"serverConnectivityConnected": "Servidor conectado",
"serverInfo": "Información del servidor",
"invitationLabel": "Invitación",
"serverLabel": "Servidor",
"search": "Búsqueda...",
"cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.",
"cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.",
"cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.",
"blocked": "Bloqueado",
"titlePlaceholder": "título...",
"postNewBulletinLabel": "Publicar nuevo boletín",
"newBulletinLabel": "Nuevo Boletín",
"joinGroup": "Únete al grupo",
"createGroup": "Crear perfil",
"addPeer": "Agregar Contacto",
"groupAddr": "Dirección",
"invitation": "Invitación",
"server": "Servidor",
"groupName": "Nombre del grupo",
"peerName": "Nombre",
"peerAddress": "Dirección",
"joinGroupTab": "Únete a un grupo",
"createGroupTab": "Crear un grupo",
"addPeerTab": "Agregar Contacto",
"createGroupBtn": "Crear",
"viewGroupMembershipTooltip": "Ver membresía del grupo",
"peerBlockedMessage": "Contacto bloqueado",
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
"couldNotSendMsgError": "No se pudo enviar este mensaje",
"pendingLabel": "Pendiente",
"chatBtn": "Chat",
"dontSavePeerHistory": "Eliminar historial de contacto",
"password": "Contraseña",
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
"networkStatusConnecting": "Conectando a la red y a los contactos...",
"localeIt": "Italiano",
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
"acknowledgedLabel": "Reconocido",
"blockBtn": "Bloquear contacto",
"savePeerHistory": "Guardar el historial con contacto",
"defaultProfileName": "Alicia",
"versionBuilddate": "Versión: %1 Basado en %2",
"zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)",
"settingTheme": "Tema",
"themeLight": "Claro",
"experimentsEnabled": "Experimentos habilitados",
"versionTor": "Versión %1 con tor %2",
"localeEs": "Español",
"networkStatusOnline": "En línea",
"newConnectionPaneTitle": "Nueva conexión",
"addNewItem": "Añadir un nuevo elemento a la lista",
"createGroupTitle": "Crear un grupo",
"serverLabel": "Servidor",
"groupNameLabel": "Nombre del grupo",
"defaultGroupName": "El Grupo Asombroso",
"createGroupTitle": "Crear un grupo"
"createGroupBtn": "Crear",
"copiedToClipboardNotification": "Copiado al portapapeles",
"addPeerTab": "Agregar Contacto",
"createGroupTab": "Crear un grupo",
"peerAddress": "Dirección",
"peerName": "Nombre",
"groupName": "Nombre del grupo",
"server": "Servidor",
"invitation": "Invitación",
"groupAddr": "Dirección",
"addPeer": "Agregar Contacto",
"createGroup": "Crear perfil",
"joinGroup": "Únete al grupo",
"newBulletinLabel": "Nuevo Boletín",
"postNewBulletinLabel": "Publicar nuevo boletín",
"titlePlaceholder": "título...",
"pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...",
"blocked": "Bloqueado",
"search": "Búsqueda...",
"invitationLabel": "Invitación",
"serverInfo": "Información del servidor",
"serverConnectivityConnected": "Servidor conectado",
"serverConnectivityDisconnected": "Servidor desconectado",
"serverSynced": "Sincronizado",
"serverNotSynced": "Fuera de sincronización con el servidor",
"viewServerInfo": "Información del servidor",
"saveBtn": "Guardar",
"inviteToGroupLabel": "Invitar al grupo",
"inviteBtn": "Invitar",
"deleteBtn": "Eliminar",
"update": "Actualizar",
"searchList": "Buscar en la lista",
"addListItemBtn": "Agregar artículo",
"membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo",
"dmTooltip": "Haz clic para enviar mensaje directo",
"copyBtn": "Copiar",
"copiedClipboardNotification": "Copiado al portapapeles",
"newGroupBtn": "Crear un nuevo grupo de chat",
"acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ",
"acceptGroupBtn": "Aceptar",
"rejectGroupBtn": "Rechazar",
"listsBtn": "Listas",
"puzzleGameBtn": "Juego de rompecabezas",
"addressLabel": "Dirección",
"displayNameLabel": "Nombre de Usuario",
"addProfileTitle": "Agregar nuevo perfil",
"editProfileTitle": "Editar perfil",
"profileName": "Nombre de Usuario",
"newProfile": "Nuevo perfil",
"editProfile": "Editar perfil",
"radioUsePassword": "Contraseña",
"noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados",
"password2Label": "Vuelve a ingresar tu contraseña",
"yourDisplayName": "Tu nombre de usuario",
"currentPasswordLabel": "Contraseña actual",
"password1Label": "Contraseña",
"passwordErrorEmpty": "El campo de contraseña no puede estar vacío",
"createProfileBtn": "Crear perfil",
"saveProfileBtn": "Guardar perfil",
"passwordErrorMatch": "Las contraseñas no coinciden",
"passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada",
"deleteProfileBtn": "Eliminar Perfil",
"deleteConfirmLabel": "Escribe ELIMINAR para confirmar",
"deleteProfileConfirmBtn": "Confirmar eliminar perfil",
"deleteConfirmText": "ELIMINAR",
"addNewProfileBtn": "Agregar nuevo perfil",
"error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña",
"yourProfiles": "Tus perfiles",
"yourServers": "Tus servidores",
"unlock": "Desbloquear",
"cwtchSettingsTitle": "Configuración de Cwtch",
"blockUnknownLabel": "Bloquear conexiones desconocidas",
"settingLanguage": "Idioma",
"localeEn": "Inglés",
"settingInterfaceZoom": "Nivel de zoom",
"largeTextLabel": "Grande",
"version": "Versión %1",
"networkStatusDisconnected": "Sin conexión, comprueba tu conexión",
"networkStatusAttemptingTor": "Intentando conectarse a la red Tor"
}

View File

@ -1,6 +1,108 @@
{
"@@locale": "fr",
"@@last_modified": "2021-07-07T23:42:20+02:00",
"@@last_modified": "2021-12-20T09:20:03+01:00",
"msgConfirmSend": "Êtes-vous sûr de vouloir envoyer",
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe de",
"msgFileTooBig": "La taille du fichier ne peut pas dépasser 10 Go",
"msgAddToAccept": "Ajoutez ce compte à vos contacts afin d'accepter ce fichier.",
"btnSendFile": "Envoyer le fichier",
"storageMigrationModalMessage": "Migration des profils vers un nouveau format de stockage. Cela peut prendre quelques minutes...",
"loadingCwtch": "Chargement de Cwtch...",
"experimentClickableLinksDescription": "L'expérience des liens cliquables vous permet de cliquer sur les URLs partagés dans les messages",
"themeNameWitch": "Sorcière",
"themeNameVampire": "Vampire",
"themeNamePumpkin": "Citrouille",
"themeNameNeon2": "Néon2",
"themeNameNeon1": "Néon1",
"themeNameMidnight": "Minuit",
"themeNameMermaid": "Sirène",
"themeNameGhost": "Fantôme",
"themeNameCwtch": "Cwtch",
"themeColorLabel": "Thème de couleur",
"settingImagePreviewsDescription": "Les images seront téléchargées et prévisualisées automatiquement. Veuillez noter que la prévisualisation des images peut souvent conduire à des failles de sécurité, et vous ne devriez pas activer cette expérience si vous utilisez Cwtch avec des contacts non fiables. Les images de profil sont prévues pour Cwtch 1.6.",
"settingImagePreviews": "Aperçu des images et photos de profil",
"settingDownloadFolder": "Dossier de téléchargement",
"enableExperimentClickableLinks": "Activer les liens cliquables",
"serverMetricsLabel": "Métriques du serveur",
"serverTotalMessagesLabel": "Nombre total de messages",
"serverConnectionsLabel": "Connexion",
"manageKnownServersShort": "Serveurs",
"manageKnownServersLong": "Gérer les serveurs connus",
"manageKnownServersButton": "Gérer les serveurs connus",
"importLocalServerSelectText": "Sélectionnez le serveur local",
"importLocalServerLabel": "Importer un serveur hébergé localement",
"importLocalServerButton": "Importer %1",
"groupsOnThisServerLabel": "Les groupes dont je fais partie sont hébergés sur ce serveur",
"fieldDescriptionLabel": "Description",
"displayNameTooltip": "Veuillez entrer un nom d'usage s'il vous plaît",
"savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au contact.",
"newMessagesLabel": "Nouveaux messages",
"localeRU": "Russe",
"copyServerKeys": "Copier les clés",
"verfiyResumeButton": "Vérifier\/reprendre",
"fileSavedTo": "Enregistré dans",
"fileInterrupted": "Interrompu",
"fileCheckingStatus": "Vérification de l'état du téléchargement",
"plainServerDescription": "Nous vous recommandons de protéger vos serveurs Cwtch par un mot de passe. Si vous ne définissez pas de mot de passe sur ce serveur, toute personne ayant accès à cet appareil peut être en mesure d'accéder aux informations concernant ce serveur, y compris les clés cryptographiques sensibles.",
"enterCurrentPasswordForDeleteServer": "Veuillez saisir le mot de passe actuel pour supprimer ce serveur",
"encryptedServerDescription": "Le chiffrement dun serveur avec un mot de passe le protège des autres personnes qui peuvent également utiliser cet appareil. Les serveurs cryptés ne peuvent pas être déchiffrés, affichés ou accessibles tant que le mot de passe correct nest pas entré pour les déverrouiller.",
"deleteServerSuccess": "Le serveur a été supprimé avec succès",
"deleteServerConfirmBtn": "Supprimer vraiment le serveur",
"unlockServerTip": "Veuillez créer ou déverrouiller un serveur pour commencer !",
"unlockProfileTip": "Veuillez créer ou déverrouiller un profil pour commencer !",
"settingServersDescription": "L'expérience des serveurs d'hébergement permet d'héberger et de gérer les serveurs Cwtch.",
"settingServers": "Serveurs d'hébergement",
"serversManagerTitleShort": "Serveurs",
"serversManagerTitleLong": "Serveurs que vous hébergez",
"serverEnabledDescription": "Démarrer ou arrêter le serveur",
"serverEnabled": "Serveur activé",
"serverDescriptionLabel": "Description du serveur",
"serverDescriptionDescription": "Votre description du serveur est à des fins de gestion personnelle uniquement, elle ne sera jamais partagée.",
"serverAutostartLabel": "Démarrage automatique",
"serverAutostartDescription": "Contrôle si l'application lance automatiquement le serveur au démarrage.",
"serverAddress": "Adresse du serveur",
"saveServerButton": "Enregistrer le serveur",
"enterServerPassword": "Entrez le mot de passe pour déverrouiller le serveur",
"editServerTitle": "Modifier le serveur",
"copyAddress": "Copier l'adresse",
"addServerTooltip": "Ajouter un nouveau serveur",
"addServerTitle": "Ajouter un serveur",
"titleManageProfilesShort": "Profils",
"descriptionStreamerMode": "Si elle est activée, cette option donne un rendu visuel plus privé à l'application pour la diffusion en direct ou la présentation, par exemple, en masquant profil et adresses de contacts.",
"tooltipSendFile": "Envoyer le fichier",
"settingFileSharing": "Partage de fichiers",
"retrievingManifestMessage": "Récupération des informations sur le fichier...",
"openFolderButton": "Ouvrir le dossier",
"messageFileSent": "Vous avez envoyé un fichier",
"messageFileOffered": "Contact vous propose de vous envoyer un fichier",
"messageEnableFileSharing": "Activez l'expérience de partage de fichiers pour afficher ce message.",
"labelFilesize": "Taille",
"labelFilename": "Nom de fichier",
"downloadFileButton": "Télécharger",
"descriptionFileSharing": "L'expérience de partage de fichiers vous permet d'envoyer et de recevoir des fichiers à partir de contacts et de groupes Cwtch. Notez que si vous partagez un fichier avec un groupe, les membres de ce groupe se connecteront avec vous directement via Cwtch pour le télécharger.",
"streamerModeLabel": "Mode Streamer\/Présentation",
"addPeer": "Ajouter le contact",
"networkStatusConnecting": "Connexion au réseau et aux contacts...",
"profileOnionLabel": "Envoyez cette adresse aux personnes avec lesquelles vous souhaitez entrer en contact.",
"addPeerTab": "Ajouter un contact",
"peerNotOnline": "Le contact est hors ligne. Les applications ne peuvent pas être utilisées pour le moment.",
"peerBlockedMessage": "Le contact est bloqué",
"peerOfflineMessage": "Le contact est hors ligne, les messages ne peuvent pas être transmis pour le moment.",
"blockBtn": "Bloquer le contact",
"savePeerHistory": "Enregistrer l'historique",
"dontSavePeerHistory": "Supprimer l'historique",
"unblockBtn": "Débloquer le contact",
"blockUnknownLabel": "Bloquer les pairs inconnus",
"blockUnknownConnectionsEnabledDescription": "Les connexions provenant de contacts inconnus sont bloquées. Vous pouvez modifier cela dans les paramètres",
"archiveConversation": "Archiver cette conversation",
"blockedMessageMessage": "Ce message provient d'un profil que vous avez bloqué.",
"showMessageButton": "Afficher le message",
"placeholderEnterMessage": "saisissez un message",
"encryptedProfileDescription": "Le chiffrement d'un profil à l'aide d'un mot de passe le protège des autres personnes susceptibles d'utiliser également cet appareil. Les profils chiffrés ne peuvent pas être déchiffrés , affichés ou accessibles tant que le mot de passe correct n'a pas été saisi pour les déverrouiller.",
"plainProfileDescription": "Nous vous recommandons de protéger vos profils Cwtch par un mot de passe. Si vous ne définissez pas de mot de passe sur ce profil, toute personne ayant accès à cet appareil peut être en mesure d'accéder aux informations relatives à ce profil, y compris les contacts, les messages et les clés de chiffrement sensibles.",
"addContactConfirm": "Ajouter le contact %1",
"contactGoto": "Aller à la conversation avec %1",
"addContact": "Ajouter le contact",
"settingUIColumnOptionSame": "Même réglage que pour le mode portrait",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
@ -8,136 +110,158 @@
"settingUIColumnLandscape": "Colonnes de l'interface utilisateur en mode paysage",
"settingUIColumnPortrait": "Colonnes de l'interface utilisateur en mode portrait",
"localePl": "Polonais",
"tooltipRemoveThisQuotedMessage": "Supprimer le message cité.",
"tooltipReplyToThisMessage": "Répondre à ce message",
"tooltipRejectContactRequest": "Refuser cette demande de contact",
"tooltipAcceptContactRequest": "Acceptez cette demande de contact.",
"notificationNewMessageFromGroup": "Nouveau message dans un groupe !",
"notificationNewMessageFromPeer": "Nouveau message d'un contact !",
"tooltipHidePassword": "Masquer le mot de passe",
"tooltipShowPassword": "Afficher le mot de passe",
"serverNotSynced": "Synchronisation des nouveaux messages (Cela peut prendre un certain temps)...",
"groupInviteSettingsWarning": "Vous avez été invité à rejoindre un groupe ! Veuillez activer l'expérience de discussion de groupe dans les paramètres pour afficher cette invitation.",
"shutdownCwtchAction": "Arrêt de Cwtch",
"shutdownCwtchDialog": "Êtes-vous sûr de vouloir arrêter Cwtch ? Ceci fermera toutes les connexions, et quittera l'application.",
"shutdownCwtchDialogTitle": "Arrêter Cwtch ?",
"tooltipRemoveThisQuotedMessage": "Supprimer le message cité.",
"deleteProfileConfirmBtn": "Supprimer vraiment le profil ?",
"groupNameLabel": "Nom du groupe",
"defaultGroupName": "Un groupe génial",
"inviteToGroupLabel": "Inviter au groupe",
"membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être représentatives de l'ensemble des membres du groupe.",
"shutdownCwtchTooltip": "Arrêt de Cwtch",
"malformedMessage": "Message mal formé",
"profileDeleteSuccess": "Le profil a été supprimé avec succès",
"debugLog": "Activer le journal de la console de débogage",
"torNetworkStatus": "Statut du réseau Tor",
"addContactFirst": "Ajoutez ou choisissez un contact pour commencer à discuter.",
"createProfileToBegin": "Veuillez créer ou déverrouiller un profil pour commencer",
"nickChangeSuccess": "Le pseudo du profil a été modifié avec succès",
"addServerFirst": "Vous devez ajouter un serveur avant de pouvoir créer un groupe.",
"deleteProfileSuccess": "Le profil a été supprimé avec succès",
"sendInvite": "Envoyer une invitation à un contact ou à un groupe",
"sendMessage": "Envoyer un message",
"cancel": "Annuler",
"resetTor": "Réinitialiser",
"torStatus": "Statut de Tor",
"torVersion": "Version de Tor",
"sendAnInvitation": "Vous avez envoyé une invitation pour : ",
"contactSuggestion": "Il s'agit d'une suggestion de contact pour : ",
"rejected": "Rejeté !",
"accepted": "Accepté !",
"chatHistoryDefault": "Cette conversation sera supprimée lorsque Cwtch sera fermé ! L'historique des messages peut être activé pour la conversation via le menu Paramètres en haut à droite.",
"newPassword": "Nouveau mot de passe",
"yesLeave": "Oui, quittez cette conversation",
"reallyLeaveThisGroupPrompt": "Êtes-vous sûr de vouloir quitter cette conversation ? Tous les messages et attributs seront supprimés.",
"leaveGroup": "Quittez cette conversation",
"inviteToGroup": "Vous avez été invité à rejoindre un groupe :",
"shutdownCwtchAction": "Arrêt de Cwtch",
"deleteBtn": "Effacer",
"acknowledgedLabel": "Accusé de réception",
"zoomLabel": "Zoom de l'interface (affecte principalement la taille du texte et des boutons)",
"localeIt": "Italien",
"versionTor": "Version %1 avec tor %2",
"version": "Version %1",
"builddate": "Construit le : %2",
"versionBuilddate": "Version : %1 Construite le : %2",
"tooltipAcceptContactRequest": "Acceptez cette demande de contact.",
"tooltipRejectContactRequest": "Refuser cette demande de contact",
"addNewItem": "Ajouter un nouvel élément à la liste",
"localeEs": "Espagnol",
"todoPlaceholder": "À faire...",
"pasteAddressToAddContact": "Collez une adresse cwtch, une invitation ou un ensemble de clés ici pour ajouter une nouvelle conversation",
"tooltipAddContact": "Ajouter un nouveau contact ou une nouvelle conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Gérer les serveurs",
"dateNever": "Jamais",
"addListItem": "Ajouter un nouvel élément de liste",
"cycleMorphsAndroid": "Cliquez pour faire défiler les morphes.\n Appuyez longuement pour réinitialiser.",
"cycleMorphsDesktop": "Cliquez pour faire défiler les morphes.\n Faites un clic droit pour réinitialiser.",
"debugLog": "Activer le journal de la console de débogage",
"joinGroupTab": "Rejoindre un groupe",
"createGroupTab": "Créer un groupe",
"peerAddress": "Adresse",
"peerName": "Nom",
"groupName": "Nom du groupe",
"server": "Serveur",
"invitation": "Invitation",
"cycleCatsAndroid": "Cliquez pour faire défiler les catégories.\nAppuyez longuement pour réinitialiser.",
"cycleCatsDesktop": "Cliquez pour parcourir la catégorie.\n Faites un clic droit pour réinitialiser.",
"cycleColoursAndroid": "Cliquez pour faire défiler les couleurs.\nAppuyez longuement pour réinitialiser.",
"groupAddr": "Adresse",
"createGroup": "Créer un groupe",
"joinGroup": "Rejoindre le groupe",
"blocked": "Bloqué",
"cycleColoursDesktop": "Cliquez pour faire défiler les couleurs.\nCliquez avec le bouton droit de la souris pour réinitialiser.",
"search": "Recherche...",
"serverInfo": "Informations sur le serveur",
"serverConnectivityConnected": "Serveur connecté",
"serverConnectivityDisconnected": "Serveur déconnecté",
"serverSynced": "Synchronisé",
"serverNotSynced": "Synchronisation des nouveaux messages (Cela peut prendre un certain temps)...",
"viewServerInfo": "Informations sur le serveur",
"update": "Mise à jour",
"searchList": "Liste de recherche",
"addListItemBtn": "Ajouter un élément",
"addProfileTitle": "Ajouter un nouveau profil",
"editProfileTitle": "Modifier le profil",
"profileName": "Pseudo",
"defaultProfileName": "Alice",
"newProfile": "Nouveau profil",
"deleteConfirmText": "SUPPRIMER",
"deleteConfirmLabel": "Tapez SUPPRIMER pour confirmer",
"addNewProfileBtn": "Ajouter un nouveau profil",
"enterProfilePassword": "Entrez un mot de passe pour consulter vos profils",
"editProfile": "Modifier le profil",
"radioUsePassword": "Mot de passe",
"radioNoPassword": "Non chiffré (pas de mot de passe)",
"saveProfileBtn": "Sauvegarder le profil",
"passwordErrorMatch": "Les mots de passe ne correspondent pas",
"passwordChangeError": "Erreur lors de la modification du mot de passe : le mot de passe fourni est rejeté",
"deleteProfileBtn": "Supprimer le profil",
"password": "Mot de passe",
"error0ProfilesLoadedForPassword": "Aucun profils chargés avec ce mot de passe",
"yourProfiles": "Vos profils",
"yourServers": "Vos serveurs",
"unlock": "Déverrouiller",
"settingLanguage": "Langue",
"localeEn": "Anglais",
"localeFr": "Français",
"localePt": "Portugais",
"localeDe": "Allemand",
"settingInterfaceZoom": "Niveau de zoom",
"settingTheme": "Thème",
"themeLight": "Clair",
"themeDark": "Sombre",
"experimentsEnabled": "Activer les expériences",
"dateLastYear": "L'année dernière",
"dateYesterday": "Hier",
"dateLastMonth": "Le mois dernier",
"dateRightNow": "Maintenant",
"successfullAddedContact": "Ajouté avec succès ",
"descriptionBlockUnknownConnections": "Si elle est activée, cette option fermera automatiquement les connexions des utilisateurs de Cwtch qui n'ont pas été ajoutés à votre liste de contacts.",
"descriptionExperimentsGroups": "L'expérience de groupe permet à Cwtch de se connecter à une infrastructure de serveurs non fiables pour faciliter la communication avec plus d'un contact.",
"descriptionExperiments": "Les expériences de Cwtch sont des fonctionnalités optionnelles et facultatives qui ajoutent des fonctionnalités supplémentaires à Cwtch et qui peuvent avoir des considérations de confidentialité différentes de celles du chat traditionnel résistant aux métadonnées 1:1, par exemple le chat de groupe, l'intégration de robots, etc.",
"titleManageProfiles": "Gérer les profils Cwtch",
"tooltipUnlockProfiles": "Déverrouillez les profils chiffrés en saisissant leur mot de passe.",
"tooltipOpenSettings": "Ouvrez le volet des paramètres",
"dateNever": "Jamais",
"titleManageServers": "Gérer les serveurs",
"inviteToGroup": "Vous avez été invité à rejoindre un groupe :",
"leaveGroup": "Quittez cette conversation",
"reallyLeaveThisGroupPrompt": "Êtes-vous sûr de vouloir quitter cette conversation ? Tous les messages et attributs seront supprimés.",
"yesLeave": "Oui, quittez cette conversation",
"noPasswordWarning": "Ne pas utiliser de mot de passe sur ce compte signifie que toutes les données stockées localement ne seront pas chiffrées.",
"yourDisplayName": "Pseudo",
"currentPasswordLabel": "Mot de passe actuel",
"password1Label": "Mot de passe",
"password2Label": "Saisissez à nouveau le mot de passe",
"passwordErrorEmpty": "Le mot de passe ne peut pas être vide",
"createProfileBtn": "Créer un profil",
"loadingTor": "Chargement de tor...",
"viewGroupMembershipTooltip": "Afficher les membres du groupe",
"networkStatusDisconnected": "Déconnecté d'Internet, vérifiez votre connexion",
"networkStatusAttemptingTor": "Tentative de connexion au réseau Tor",
"networkStatusOnline": "En ligne",
"newConnectionPaneTitle": "Nouvelle connexion",
"enableGroups": "Activer la discussion de groupe",
"enterCurrentPasswordForDelete": "Veuillez entrer le mot de passe actuel pour supprimer ce profil.",
"conversationSettings": "Paramètres de conversation",
"invalidImportString": "Chaîne d'importation non valide",
"contactAlreadyExists": "Le contact existe déjà",
"conversationSettings": "Paramètres de conversation",
"enterCurrentPasswordForDelete": "Veuillez entrer le mot de passe actuel pour supprimer ce profil.",
"enableGroups": "Activer la discussion de groupe",
"experimentsEnabled": "Activer les expériences",
"localeIt": "Italien",
"localeEs": "Espagnol",
"addListItem": "Ajouter un nouvel élément de liste",
"addNewItem": "Ajouter un nouvel élément à la liste",
"todoPlaceholder": "À faire...",
"newConnectionPaneTitle": "Nouvelle connexion",
"networkStatusOnline": "En ligne",
"networkStatusConnecting": "Se connecter au réseau et aux pairs...",
"networkStatusAttemptingTor": "Tentative de connexion au réseau Tor",
"networkStatusDisconnected": "Déconnecté d'Internet, vérifiez votre connexion",
"viewGroupMembershipTooltip": "Afficher les membres du groupe",
"loadingTor": "Chargement de tor...",
"tooltipOpenSettings": "Ouvrez le volet des paramètres",
"tooltipAddContact": "Ajouter un nouveau contact ou une nouvelle conversation",
"titleManageContacts": "Conversations",
"tooltipUnlockProfiles": "Déverrouillez les profils chiffrés en saisissant leur mot de passe.",
"titleManageProfiles": "Gérer les profils Cwtch",
"descriptionExperiments": "Les expériences de Cwtch sont des fonctionnalités optionnelles et facultatives qui ajoutent des fonctionnalités supplémentaires à Cwtch et qui peuvent avoir des considérations de confidentialité différentes de celles du chat traditionnel résistant aux métadonnées 1:1, par exemple le chat de groupe, l'intégration de robots, etc.",
"descriptionExperimentsGroups": "L'expérience de groupe permet à Cwtch de se connecter à une infrastructure de serveurs non fiables pour faciliter la communication avec plus d'un contact.",
"descriptionBlockUnknownConnections": "Si elle est activée, cette option fermera automatiquement les connexions des utilisateurs de Cwtch qui n'ont pas été ajoutés à votre liste de contacts.",
"successfullAddedContact": "Ajouté avec succès ",
"dateRightNow": "Maintenant",
"dateLastMonth": "Le mois dernier",
"dateYesterday": "Hier",
"newPassword": "Nouveau mot de passe",
"chatHistoryDefault": "Cette conversation sera supprimée lorsque Cwtch sera fermé ! L'historique des messages peut être activé pour la conversation via le menu Paramètres en haut à droite.",
"accepted": "Accepté !",
"rejected": "Rejeté !",
"contactSuggestion": "Il s'agit d'une suggestion de contact pour : ",
"sendAnInvitation": "Vous avez envoyé une invitation pour : ",
"torVersion": "Version de Tor",
"torStatus": "Statut de Tor",
"resetTor": "Réinitialiser",
"cancel": "Annuler",
"sendMessage": "Envoyer un message",
"sendInvite": "Envoyer une invitation à un contact ou à un groupe",
"deleteProfileSuccess": "Le profil a été supprimé avec succès",
"addServerFirst": "Vous devez ajouter un serveur avant de pouvoir créer un groupe.",
"nickChangeSuccess": "Le pseudo du profil a été modifié avec succès",
"createProfileToBegin": "Veuillez créer ou déverrouiller un profil pour commencer",
"addContactFirst": "Ajoutez ou choisissez un contact pour commencer à discuter.",
"torNetworkStatus": "Statut du réseau Tor",
"profileDeleteSuccess": "Le profil a été supprimé avec succès",
"malformedMessage": "Message mal formé",
"shutdownCwtchDialogTitle": "Arrêter Cwtch ?",
"shutdownCwtchDialog": "Êtes-vous sûr de vouloir arrêter Cwtch ? Ceci fermera toutes les connexions, et quittera l'application.",
"groupInviteSettingsWarning": "Vous avez été invité à rejoindre un groupe ! Veuillez activer l'expérience de discussion de groupe dans les paramètres pour afficher cette invitation.",
"tooltipShowPassword": "Afficher le mot de passe",
"tooltipHidePassword": "Masquer le mot de passe",
"notificationNewMessageFromPeer": "Nouveau message d'un contact !",
"notificationNewMessageFromGroup": "Nouveau message dans un groupe !",
"smallTextLabel": "Petit",
"defaultScalingText": "Taille par défaut du texte (échelle:",
"builddate": "Construit le : %2",
"version": "Version %1",
"versionTor": "Version %1 avec tor %2",
"themeDark": "Sombre",
"themeLight": "Clair",
"settingTheme": "Thème",
"largeTextLabel": "Large",
"settingInterfaceZoom": "Niveau de zoom",
"localeDe": "Allemand",
"localePt": "Portugais",
"localeFr": "Français",
"localeEn": "Anglais",
"settingLanguage": "Langue",
"blockUnknownLabel": "Bloquer les pairs inconnus",
"zoomLabel": "Zoom de l'interface (affecte principalement la taille du texte et des boutons)",
"versionBuilddate": "Version : %1 Construite le : %2",
"cwtchSettingsTitle": "Préférences Cwtch",
"unlock": "Déverrouiller",
"yourServers": "Vos serveurs",
"yourProfiles": "Vos profils",
"error0ProfilesLoadedForPassword": "Aucun profils chargés avec ce mot de passe",
"password": "Mot de passe",
"enterProfilePassword": "Entrez un mot de passe pour consulter vos profils",
"addNewProfileBtn": "Ajouter un nouveau profil",
"deleteConfirmText": "SUPPRIMER",
"deleteProfileConfirmBtn": "Supprimer vraiment le profil ?",
"deleteConfirmLabel": "Tapez SUPPRIMER pour confirmer",
"deleteProfileBtn": "Supprimer le profil",
"passwordChangeError": "Erreur lors de la modification du mot de passe : le mot de passe fourni est rejeté",
"passwordErrorMatch": "Les mots de passe ne correspondent pas",
"saveProfileBtn": "Sauvegarder le profil",
"createProfileBtn": "Créer un profil",
"passwordErrorEmpty": "Le mot de passe ne peut pas être vide",
"password2Label": "Saisissez à nouveau le mot de passe",
"password1Label": "Mot de passe",
"currentPasswordLabel": "Mot de passe actuel",
"yourDisplayName": "Pseudo",
"profileOnionLabel": "Envoyez cette adresse aux personnes avec lesquelles vous souhaitez entrer en contact.",
"noPasswordWarning": "Ne pas utiliser de mot de passe sur ce compte signifie que toutes les données stockées localement ne seront pas chiffrées.",
"radioNoPassword": "Non chiffré (pas de mot de passe)",
"radioUsePassword": "Mot de passe",
"copiedToClipboardNotification": "Copié dans le presse-papier",
"copyBtn": "Copier",
"editProfile": "Modifier le profil",
"newProfile": "Nouveau profil",
"defaultProfileName": "Alice",
"profileName": "Pseudo",
"editProfileTitle": "Modifier le profil",
"addProfileTitle": "Ajouter un nouveau profil",
"deleteBtn": "Supprimer",
"unblockBtn": "Débloquer le pair",
"dontSavePeerHistory": "Supprimer l'historique des pairs",
"savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au pair.",
"savePeerHistory": "Sauvegarder l'historique des pairs",
"blockBtn": "Bloquer le pair",
"saveBtn": "Sauvegarder",
"displayNameLabel": "Pseudo",
"addressLabel": "Adresse",
@ -147,54 +271,18 @@
"chatBtn": "Discuter",
"rejectGroupBtn": "Refuser",
"acceptGroupBtn": "Accepter",
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe",
"newGroupBtn": "Créer un nouveau groupe",
"copiedClipboardNotification": "Copié dans le presse-papier",
"peerOfflineMessage": "Le pair est hors ligne, les messages ne peuvent pas être remis pour le moment",
"peerBlockedMessage": "Le pair est bloqué",
"copyBtn": "Copier",
"pendingLabel": "En attente",
"acknowledgedLabel": "Accusé de réception",
"couldNotSendMsgError": "Impossible d'envoyer ce message",
"dmTooltip": "Envoyer un message privé",
"membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être représentatives de l'ensemble des membres du groupe.",
"addListItemBtn": "Ajouter un élément",
"peerNotOnline": "Le pair est hors ligne, les messages ne peuvent pas être remis pour le moment",
"searchList": "Liste de recherche",
"update": "Mise à jour",
"inviteBtn": "Invitation",
"inviteToGroupLabel": "Inviter au groupe",
"groupNameLabel": "Nom du groupe",
"viewServerInfo": "Informations sur le serveur",
"serverSynced": "Synchronisé",
"serverConnectivityDisconnected": "Serveur déconnecté",
"serverConnectivityConnected": "Serveur connecté",
"serverInfo": "Informations sur le serveur",
"invitationLabel": "Invitation",
"serverLabel": "Serveur",
"search": "Recherche...",
"cycleColoursDesktop": "Cliquez pour faire défiler les couleurs.\nCliquez avec le bouton droit de la souris pour réinitialiser.",
"cycleColoursAndroid": "Cliquez pour faire défiler les couleurs.\nAppuyez longuement pour réinitialiser.",
"cycleMorphsDesktop": "Cliquez pour faire défiler les morphes.\n Faites un clic droit pour réinitialiser.",
"cycleMorphsAndroid": "Cliquez pour faire défiler les morphes.\n Appuyez longuement pour réinitialiser.",
"cycleCatsDesktop": "Cliquez pour parcourir la catégorie.\n Faites un clic droit pour réinitialiser.",
"cycleCatsAndroid": "Cliquez pour faire défiler les catégories.\nAppuyez longuement pour réinitialiser.",
"blocked": "Bloqué",
"titlePlaceholder": "titre...",
"postNewBulletinLabel": "Envoyer un nouveau bulletin",
"newBulletinLabel": "Nouveau bulletin",
"joinGroup": "Rejoindre le groupe",
"createGroup": "Créer un groupe",
"addPeer": "Ajouter un pair",
"groupAddr": "Adresse",
"invitation": "Invitation",
"server": "Serveur",
"groupName": "Nom du groupe",
"peerName": "Nom",
"peerAddress": "Adresse",
"joinGroupTab": "Rejoindre un groupe",
"createGroupTab": "Créer un groupe",
"addPeerTab": "Ajouter un pair",
"createGroupBtn": "Créer",
"defaultGroupName": "Un groupe génial",
"createGroupTitle": "Créer un groupe"
}

View File

@ -1,200 +1,288 @@
{
"@@locale": "it",
"@@last_modified": "2021-07-07T23:42:20+02:00",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
"settingUIColumnSingle": "Single",
"settingUIColumnLandscape": "UI Columns in Landscape Mode",
"settingUIColumnPortrait": "UI Columns in Portrait Mode",
"localePl": "Polish",
"tooltipRemoveThisQuotedMessage": "Remove quoted message.",
"tooltipReplyToThisMessage": "Reply to this message",
"tooltipRejectContactRequest": "Rifiuta questa richiesta di contatto",
"tooltipAcceptContactRequest": "Accetta questa richiesta di contatto.",
"notificationNewMessageFromGroup": "Nuovo messaggio in un gruppo!",
"notificationNewMessageFromPeer": "Nuovo messaggio da un contatto!",
"tooltipHidePassword": "Nascondi la password",
"tooltipShowPassword": "Mostra la password",
"@@last_modified": "2021-12-20T09:20:03+01:00",
"blockUnknownLabel": "Blocca Contatti Sconosciuti",
"unblockBtn": "Sblocca il Contatto",
"dontSavePeerHistory": "Elimina Cronologia",
"blockBtn": "Blocca il Contatto",
"addPeer": "Aggiungi Contatto",
"importLocalServerSelectText": "Seleziona il Server Locale",
"manageKnownServersButton": "Gestisci i Server Conosciuti",
"manageKnownServersLong": "Gestisci i Server Conosciuti",
"serverMetricsLabel": "Metriche del Server",
"serverTotalMessagesLabel": "Numero Totale di Messaggi",
"enableExperimentClickableLinks": "Abilita Link Cliccabili",
"settingDownloadFolder": "Cartella di Download",
"themeColorLabel": "Schema di Colori",
"btnSendFile": "Invia File",
"newMessagesLabel": "Nuovi Messaggi",
"copiedToClipboardNotification": "Copiato negli Appunti",
"groupNameLabel": "Nome del gruppo",
"titleManageServers": "Gestisci i Server",
"leaveGroup": "Lascia Questa Conversazione",
"yesLeave": "Sì, Lascia Questa Conversazione",
"newPassword": "Nuova Password",
"sendMessage": "Invia Messaggio",
"tooltipShowPassword": "Mostra la Password",
"tooltipHidePassword": "Nascondi la Password",
"showMessageButton": "Mostra il Messaggio",
"addServerTitle": "Aggiungi Server",
"editServerTitle": "Modifica il Server",
"serverAddress": "Indirizzo del Server",
"serverEnabled": "Server Abilitato",
"saveServerButton": "Salva il Server",
"serversManagerTitleLong": "Server che Gestisci",
"settingServers": "Server di Hosting",
"openFolderButton": "Apri Cartella",
"reallyLeaveThisGroupPrompt": "Confermi di voler lasciare questa conversazione? Tutti i messaggi e gli attributi verranno eliminati.",
"dateLastMonth": "Mese Scorso",
"dateLastYear": "L'Anno Scorso",
"titleManageProfiles": "Gestisci i Profili Cwtch",
"contactAlreadyExists": "Il Contatto Esiste Già",
"conversationSettings": "Impostazioni di Conversazione",
"enableGroups": "Abilita la Chat di Gruppo",
"addListItem": "Aggiungi un Nuovo Elemento alla Lista",
"newConnectionPaneTitle": "Nuova Connessione",
"viewGroupMembershipTooltip": "Visualizza i Membri del Gruppo",
"experimentsEnabled": "Abilita Esperimenti",
"yourServers": "I Tuoi Server",
"yourProfiles": "I Tuoi Profili",
"deleteProfileConfirmBtn": "Elimina Definitivamente il Profilo",
"deleteProfileBtn": "Elimina Profilo",
"saveProfileBtn": "Salva il Profilo",
"createProfileBtn": "Crea un Profilo",
"currentPasswordLabel": "Password Corrente",
"yourDisplayName": "Il tuo Nome Visualizzato",
"editProfile": "Modifica Profilo",
"newProfile": "Nuovo Profilo",
"editProfileTitle": "Modifica Profilo",
"puzzleGameBtn": "Gioco di Puzzle",
"searchList": "Elenco di Ricerca",
"dmTooltip": "Clicca per inviare un Messaggio Diretto",
"addListItemBtn": "Aggiungi Elemento",
"viewServerInfo": "Informazioni sul Server",
"serverConnectivityDisconnected": "Server Disconnesso",
"serverConnectivityConnected": "Server Connesso",
"newBulletinLabel": "Nuovo Bollettino",
"createGroupTitle": "Crea un Gruppo",
"defaultGroupName": "Gruppo Fantastico",
"pasteAddressToAddContact": "Incolla qui un indirizzo cwtch, un invito o un gruppo di chiavi per aggiungere una nuova conversazione",
"cycleCatsAndroid": "Clicca per scorrere le categorie.\nPressione lunga per resettare.",
"cycleCatsDesktop": "Clicca per scorrere le categorie.\nClicca con il tasto destro per resettare.",
"cycleMorphsAndroid": "Clicca per scorrere i morph.\nPressione lunga per resettare.",
"cycleMorphsDesktop": "Clicca per scorrere i morph.\nClicca con il tasto destro per resettare.",
"cycleColoursAndroid": "Clicca per scorrere i colori.\nPressione lunga per resettare.",
"cycleColoursDesktop": "Clicca per scorrere i colori.\nClicca con il tasto destro per resettare.",
"noPasswordWarning": "Se non utilizzi una password su questo account, tutti i dati archiviati localmente non verranno criptati",
"descriptionExperiments": "Gli esperimenti di Cwtch sono opzioni abilitabili per aggiungere a Cwtch funzionalità che possono considerare la privacy diversamente rispetto alla tradizionale chat 1:1 resistente ai metadati, ad esempio chat di gruppo, integrazione di bot ecc.",
"shutdownCwtchDialog": "Confermi di voler chiudere Cwtch? Questo chiuderà tutte le connessioni e uscirà dall'applicazione.",
"groupInviteSettingsWarning": "Hai ricevuto l'invito ad unirti ad un gruppo! Abilita l'Esperimento di chat di gruppo in Impostazioni per visualizzare questo Invito.",
"addPeerTab": "Aggiungi un contatto",
"peerNotOnline": "Il contatto è offline. Le applicazioni non possono essere utilizzate in questo momento.",
"peerBlockedMessage": "Il contatto è bloccato",
"peerOfflineMessage": "Il contatto è offline, i messaggi non possono essere recapitati in questo momento",
"networkStatusConnecting": "Connessione alla rete e ai contatti...",
"savePeerHistoryDescription": "Determina se eliminare la cronologia eventualmente associata al contatto.",
"savePeerHistory": "Salva cronologia ",
"profileOnionLabel": "Invia questo indirizzo ai contatti con cui vuoi connetterti",
"importLocalServerLabel": "Importa un server ospitato localmente",
"importLocalServerButton": "Importa %1",
"groupsOnThisServerLabel": "Gruppi di cui sono parte su questo server",
"fieldDescriptionLabel": "Descrizione",
"displayNameTooltip": "Inserisci un nome visualizzato",
"manageKnownServersShort": "Server",
"serverConnectionsLabel": "Connessione",
"experimentClickableLinksDescription": "L'esperimento dei link cliccabili permette di cliccare sugli URL condivisi nei messaggi",
"settingImagePreviews": "Anteprime delle immagini e immagini del profilo",
"settingImagePreviewsDescription": "Le immagini verranno scaricate e visualizzate in anteprima automaticamente. Tieni presente che le anteprime delle immagini possono spesso portare a vulnerabilità di sicurezza e non dovresti abilitare questo esperimento se usi Cwtch con contatti non attendibili. Le immagini del profilo sono previste per Cwtch 1.6.",
"themeNameCwtch": "Cwtch",
"themeNameWitch": "Strega",
"themeNameVampire": "Vampiro",
"themeNameGhost": "Fantasma",
"themeNamePumpkin": "Zucca",
"themeNameMermaid": "Sirena",
"themeNameMidnight": "Mezzanotte",
"themeNameNeon1": "Neon1",
"themeNameNeon2": "Neon2",
"loadingCwtch": "Caricamento Cwtch...",
"storageMigrationModalMessage": "Migrazione dei profili al nuovo formato di archiviazione. L'operazione potrebbe richiedere alcuni minuti...",
"msgAddToAccept": "Aggiungi questo account ai tuoi contatti per accettare questo file.",
"msgConfirmSend": "Confermi di voler inviare?",
"msgFileTooBig": "La dimensione del file non può superare i 10 GB",
"descriptionStreamerMode": "Se attivata, questa opzione rende l'applicazione visivamente più privata per lo streaming o la presentazione, ad esempio nascondendo il profilo e gli indirizzi di contatto",
"retrievingManifestMessage": "Recupero delle informazioni sul file in corso...",
"downloadFileButton": "Scarica",
"labelFilename": "Nome del file",
"labelFilesize": "Dimensione",
"messageEnableFileSharing": "Abilita l'esperimento di condivisione dei file per visualizzare questo messaggio.",
"messageFileSent": "Hai inviato un file",
"titleManageProfilesShort": "Profili",
"tooltipReplyToThisMessage": "Rispondi a questo messaggio",
"tooltipRemoveThisQuotedMessage": "Rimuovi il messaggio citato.",
"localePl": "Polacco",
"settingUIColumnPortrait": "Colonne dell'interfaccia utente in modalità verticale",
"settingUIColumnLandscape": "Colonne dell'interfaccia utente in modalità orizzontale",
"settingUIColumnSingle": "Singola",
"settingUIColumnDouble12Ratio": "Doppia (1:2)",
"settingUIColumnDouble14Ratio": "Doppia (1:4)",
"settingUIColumnOptionSame": "Stessa impostazione della modalità verticale",
"contactGoto": "Vai alla conversazione con %1",
"addContact": "Aggiungi contatto",
"addContactConfirm": "Aggiungi %1 come contatto",
"encryptedProfileDescription": "Criptare un profilo con una password lo protegge da altre persone che potrebbero usare questo dispositivo. I profili criptati non possono essere decriptati, visualizzati o accessibili finché non viene inserita la password corretta per sbloccarli.",
"plainServerDescription": "Ti raccomandiamo di proteggere i tuoi server Cwtch con una password. Se non imposti una password su questo server, chiunque abbia accesso a questo dispositivo potrebbe essere in grado di accedere alle relativ informazioni, compresi dati sensibili come le chiavi crittografiche.",
"plainProfileDescription": "Ti raccomandiamo di proteggere i tuoi profili Cwtch con una password. Se non imposti una password su questo profilo, chiunque abbia accesso a questo dispositivo potrebbe essere in grado di accedere alle relative informazioni, compresi contatti, messaggi e altri dati sensibili come le chiavi crittografiche.",
"placeholderEnterMessage": "Scrivi un messaggio...",
"blockedMessageMessage": "Questo messaggio proviene da un profilo che hai bloccato.",
"blockUnknownConnectionsEnabledDescription": "Le connessioni da contatti sconosciuti sono bloccate. Puoi modificare questa impostazione in Impostazioni",
"archiveConversation": "Archivia questa conversazione",
"streamerModeLabel": "Modalità Streamer\/Presentazione",
"serverDescriptionLabel": "Descrizione del server",
"serverDescriptionDescription": "La tua descrizione del server solo per gestione personale, non sarà mai condivisa",
"serverEnabledDescription": "Avvia o arresta il server",
"serverAutostartLabel": "Avvio automatico",
"serverAutostartDescription": "Controlla se l'applicazione avvierà automaticamente il server all'avvio",
"serversManagerTitleShort": "Gestisci i server",
"addServerTooltip": "Aggiungi nuovo server",
"unlockServerTip": "Crea o sblocca un server per iniziare!",
"unlockProfileTip": "Crea o sblocca un profilo per iniziare!",
"enterServerPassword": "Inserisci la password per sbloccare il server",
"settingServersDescription": "L'esperimento dei server di hosting permette di allocare e gestire i server Cwtch",
"copyAddress": "Copia indirizzo",
"enterCurrentPasswordForDeleteServer": "Inserisci la password attuale per eliminare questo server",
"deleteServerSuccess": "Server eliminato con successo",
"deleteServerConfirmBtn": "Elimina davvero il server",
"encryptedServerDescription": "Criptare un server con una password lo protegge da altre persone che potrebbero usare questo dispositivo. I server criptati non possono essere decriptati, visualizzati o accessibili finché non viene inserita la password corretta per sbloccarli.",
"fileSavedTo": "Salvato in",
"fileInterrupted": "Interrotto",
"fileCheckingStatus": "Controllo dello stato del download",
"verfiyResumeButton": "Verifica\/riprendi",
"copyServerKeys": "Copia chiavi",
"localeRU": "Russo",
"messageFileOffered": "Il contatto offre l'invio di un file",
"tooltipSendFile": "Invia file",
"settingFileSharing": "Condivisione file",
"descriptionFileSharing": "L'esperimento di condivisione dei file ti consente di inviare e ricevere file dai contatti e dai gruppi di Cwtch. Tieni presente che la condivisione di un file con un gruppo farà sì che i membri di quel gruppo si colleghino con te direttamente su Cwtch per scaricarlo.",
"serverNotSynced": "Sincronizzazione nuovi messaggi (l'operazione può richiedere del tempo)...",
"groupInviteSettingsWarning": "Sei stato invitato ad unirti ad un gruppo! Abilita l'Esperimento di chat di gruppo in Impostazioni per visualizzare questo Invito.",
"shutdownCwtchAction": "Chiudi Cwtch",
"shutdownCwtchDialog": "Sei sicuro di voler chiudere Cwtch? Questo chiuderà tutte le connessioni e uscirà dall'applicazione.",
"shutdownCwtchDialogTitle": "Chiudi Cwtch?",
"shutdownCwtchTooltip": "Chiudi Cwtch",
"malformedMessage": "Messaggio non valido",
"profileDeleteSuccess": "Profilo eliminato con successo",
"debugLog": "Attiva la registrazione del debug della console",
"torNetworkStatus": "Stato della rete Tor",
"addContactFirst": "Aggiungi o scegli un contatto per iniziare a chattare.",
"createProfileToBegin": "Crea o sblocca un profilo per iniziare",
"nickChangeSuccess": "Nickname del profilo modificato con successo",
"addServerFirst": "È necessario aggiungere un server prima di poter creare un gruppo",
"deleteProfileSuccess": "Profilo eliminato con successo",
"sendInvite": "Invia un invito a un contatto o a un gruppo",
"sendMessage": "Invia messaggio",
"cancel": "Annulla",
"resetTor": "Resettare",
"torStatus": "Stato di Tor",
"torVersion": "Versione di Tor",
"sendAnInvitation": "Hai inviato un invito per:",
"contactSuggestion": "Questo è un suggerimento di contatto per:",
"rejected": "Rifiutato!",
"accepted": "Accettato!",
"chatHistoryDefault": "Questa conversazione sarà cancellata quando Cwtch sarà chiuso! La cronologia dei messaggi può essere abilitata per ogni conversazione tramite il menu Impostazioni in alto a destra.",
"newPassword": "Nuova password",
"yesLeave": "Sì, lascia questa conversazione",
"reallyLeaveThisGroupPrompt": "Uscire da questa conversazione? Tutti i messaggi e gli attributi verranno eliminati.",
"leaveGroup": "Lascia questa conversazione",
"inviteToGroup": "Hai ricevuto un invito a unirti a un gruppo:",
"pasteAddressToAddContact": "Incolla qui un indirizzo cwtch, un invito o un mazzo di chiavi per aggiungere una nuova conversazione",
"enterCurrentPasswordForDelete": "Inserisci la password attuale per eliminare questo profilo.",
"invalidImportString": "Importazione stringa non valida",
"tooltipOpenSettings": "Aprire il pannello delle impostazioni",
"tooltipAddContact": "Aggiungi un nuovo contatto o conversazione",
"titleManageContacts": "Conversazioni",
"titleManageServers": "Gestisci i server",
"dateNever": "Mai",
"dateLastYear": "L'anno scorso",
"dateYesterday": "Ieri",
"dateLastMonth": "Mese scorso",
"dateRightNow": "Ora",
"successfullAddedContact": "Aggiunto con successo ",
"descriptionBlockUnknownConnections": "Se attivata, questa opzione chiuderà automaticamente le connessioni degli utenti Cwtch che non sono stati aggiunti alla tua lista di contatti.",
"descriptionExperimentsGroups": "L'esperimento di gruppo permette a Cwtch di connettersi con un'infrastruttura server non fidata per facilitare la comunicazione con più di un contatto.",
"descriptionExperiments": "Gli esperimenti di Cwtch sono opzioni a scelta che aggiungono a Cwtch funzionalità che possono avere diverse considerazioni sulla privacy rispetto alla tradizionale chat 1:1 resistente ai metadati, ad esempio chat di gruppo, integrazione di bot ecc.",
"titleManageProfiles": "Gestisci i profili Cwtch",
"tooltipUnlockProfiles": "Sblocca i profili crittografati inserendo la loro password.",
"tooltipOpenSettings": "Aprire il pannello delle impostazioni",
"invalidImportString": "Importazione stringa non valida",
"contactAlreadyExists": "Il contatto esiste già",
"conversationSettings": "Impostazioni di conversazione",
"enterCurrentPasswordForDelete": "Inserisci la password attuale per eliminare questo profilo.",
"enableGroups": "Abilita la chat di gruppo",
"experimentsEnabled": "Abilita esperimenti",
"descriptionExperimentsGroups": "L'esperimento di gruppo permette a Cwtch di connettersi con un'infrastruttura server non fidata per facilitare la comunicazione con più di un contatto.",
"descriptionBlockUnknownConnections": "Se attivata, questa opzione chiuderà automaticamente le connessioni degli utenti Cwtch che non sono stati aggiunti alla tua lista di contatti.",
"successfullAddedContact": "Aggiunto con successo ",
"dateRightNow": "Ora",
"dateYesterday": "Ieri",
"dateNever": "Mai",
"inviteToGroup": "Hai ricevuto un invito a unirti a un gruppo:",
"chatHistoryDefault": "Questa conversazione sarà cancellata quando Cwtch sarà chiuso! La cronologia dei messaggi può essere abilitata per ogni conversazione tramite il menu Impostazioni in alto a destra.",
"accepted": "Accettato!",
"rejected": "Rifiutato!",
"contactSuggestion": "Questo è un suggerimento di contatto per:",
"sendAnInvitation": "Hai inviato un invito per:",
"torVersion": "Versione di Tor",
"torStatus": "Stato di Tor",
"resetTor": "Resettare",
"cancel": "Annulla",
"sendInvite": "Invia un invito a un contatto o a un gruppo",
"deleteProfileSuccess": "Profilo eliminato con successo",
"addServerFirst": "È necessario aggiungere un server prima di poter creare un gruppo",
"nickChangeSuccess": "Nickname del profilo modificato con successo",
"createProfileToBegin": "Crea o sblocca un profilo per iniziare",
"addContactFirst": "Aggiungi o scegli un contatto per iniziare a chattare.",
"torNetworkStatus": "Stato della rete Tor",
"debugLog": "Attiva la registrazione del debug della console",
"profileDeleteSuccess": "Profilo eliminato con successo",
"malformedMessage": "Messaggio non valido",
"shutdownCwtchTooltip": "Chiudi Cwtch",
"shutdownCwtchDialogTitle": "Chiudi Cwtch?",
"shutdownCwtchAction": "Chiudi Cwtch",
"notificationNewMessageFromPeer": "Nuovo messaggio da un contatto!",
"notificationNewMessageFromGroup": "Nuovo messaggio in un gruppo!",
"tooltipAcceptContactRequest": "Accetta questa richiesta di contatto.",
"tooltipRejectContactRequest": "Rifiuta questa richiesta di contatto",
"versionBuilddate": "Versione: %1 Costruito il: %2",
"versionTor": "Versione %1 con tor %2",
"version": "Versione %1",
"builddate": "Costruito il: %2",
"localeEn": "Inglese",
"localeIt": "Italiano",
"localeEs": "Spagnolo",
"addListItem": "Aggiungi un nuovo elemento alla lista",
"addNewItem": "Aggiungi un nuovo elemento alla lista",
"todoPlaceholder": "Da fare...",
"newConnectionPaneTitle": "Nuova connessione",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Connessione alla rete e ai peer ...",
"networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor",
"networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione",
"viewGroupMembershipTooltip": "Visualizza i membri del gruppo",
"loadingTor": "Caricamento di tor...",
"smallTextLabel": "Piccolo",
"defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:",
"builddate": "Costruito il: %2",
"version": "Versione %1",
"versionTor": "Versione %1 con tor %2",
"themeDark": "Scuro",
"themeLight": "Chiaro",
"settingTheme": "Tema",
"largeTextLabel": "Grande",
"settingInterfaceZoom": "Livello di zoom",
"localeDe": "Tedesco",
"localePt": "Portoghese",
"localeFr": "Francese",
"localeEn": "Inglese",
"settingLanguage": "Lingua",
"blockUnknownLabel": "Blocca peer sconosciuti",
"zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)",
"versionBuilddate": "Versione: %1 Costruito il: %2",
"cwtchSettingsTitle": "Impostazioni di Cwtch",
"unlock": "Sblocca",
"yourServers": "I tuoi server",
"yourProfiles": "I tuoi profili",
"error0ProfilesLoadedForPassword": "0 profili caricati con quella password",
"password": "Password",
"enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili",
"addNewProfileBtn": "Aggiungi nuovo profilo",
"deleteConfirmText": "ELIMINA",
"deleteProfileConfirmBtn": "Elimina realmente il profilo",
"deleteConfirmLabel": "Digita ELIMINA per confermare",
"deleteProfileBtn": "Elimina profilo",
"passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata",
"passwordErrorMatch": "Le password non corrispondono",
"saveProfileBtn": "Salva il profilo",
"createProfileBtn": "Crea un profilo",
"passwordErrorEmpty": "La password non può essere vuota",
"password2Label": "Reinserire la password",
"password1Label": "Password",
"currentPasswordLabel": "Password corrente",
"yourDisplayName": "Il tuo nome visualizzato",
"profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
"noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati",
"radioNoPassword": "Non criptato (senza password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copiato negli appunti",
"copyBtn": "Copia",
"editProfile": "Modifica profilo",
"newProfile": "Nuovo profilo",
"defaultProfileName": "Alice",
"profileName": "Nome visualizzato",
"editProfileTitle": "Modifica profilo",
"addProfileTitle": "Aggiungi nuovo profilo",
"deleteBtn": "Elimina",
"unblockBtn": "Sblocca il peer",
"dontSavePeerHistory": "Elimina cronologia dei peer",
"savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
"savePeerHistory": "Salva cronologia peer",
"blockBtn": "Blocca il peer",
"saveBtn": "Salva",
"displayNameLabel": "Nome visualizzato",
"addressLabel": "Indirizzo",
"puzzleGameBtn": "Gioco di puzzle",
"bulletinsBtn": "Bollettini",
"listsBtn": "Liste",
"chatBtn": "Chat",
"rejectGroupBtn": "Rifiuta",
"acceptGroupBtn": "Accetta",
"acceptGroupInviteLabel": "Vuoi accettare l'invito a",
"newGroupBtn": "Crea un nuovo gruppo",
"copiedClipboardNotification": "Copiato negli Appunti",
"peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
"peerBlockedMessage": "Il peer è bloccato",
"pendingLabel": "In corso",
"acknowledgedLabel": "Riconosciuto",
"couldNotSendMsgError": "Impossibile inviare questo messaggio",
"dmTooltip": "Clicca per inviare un Messagio Diretto",
"membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.",
"addListItemBtn": "Aggiungi elemento",
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
"searchList": "Cerca nella lista",
"update": "Aggiornamento",
"inviteBtn": "Invitare",
"inviteToGroupLabel": "Invitare nel gruppo",
"groupNameLabel": "Nome del gruppo",
"viewServerInfo": "Informazioni sul server",
"serverSynced": "Sincronizzato",
"serverConnectivityDisconnected": "Server disconnesso",
"serverConnectivityConnected": "Server connesso",
"serverInfo": "Informazioni sul server",
"invitationLabel": "Invito",
"serverLabel": "Server",
"search": "Ricerca...",
"cycleColoursDesktop": "Fare clic per scorrere i colori.\nCliccare con il tasto destro per resettare.",
"cycleColoursAndroid": "Fare clic per scorrere i colori.\nPressione lunga per resettare.",
"cycleMorphsDesktop": "Fare clic per scorrere i morph.\nCliccare con il tasto destro per resettare.",
"cycleMorphsAndroid": "Fare clic per scorrere i morph.\nPressione lunga per resettare.",
"cycleCatsDesktop": "Fare clic per scorrere le categorie.\nCliccare con il tasto destro per resettare.",
"cycleCatsAndroid": "Fare clic per scorrere le categorie.\nPressione lunga per resettare.",
"blocked": "Bloccato",
"titlePlaceholder": "titolo...",
"postNewBulletinLabel": "Pubblica un nuovo bollettino",
"newBulletinLabel": "Nuovo bollettino",
"joinGroup": "Unisciti al gruppo",
"createGroup": "Crea un gruppo",
"addPeer": "Aggiungi peer",
"groupAddr": "Indirizzo",
"invitation": "Invito",
"server": "Server",
"groupName": "Nome del gruppo",
"peerName": "Nome",
"peerAddress": "Indirizzo",
"joinGroupTab": "Unisciti a un gruppo",
"createGroupTab": "Crea un gruppo",
"addPeerTab": "Aggiungi un peer",
"createGroupBtn": "Crea",
"defaultGroupName": "Gruppo fantastico",
"createGroupTitle": "Crea un gruppo"
"createGroupTab": "Crea un gruppo",
"joinGroupTab": "Unisciti a un gruppo",
"peerAddress": "Indirizzo",
"peerName": "Nome",
"groupName": "Nome del gruppo",
"server": "Server",
"invitation": "Invito",
"groupAddr": "Indirizzo",
"createGroup": "Crea un gruppo",
"joinGroup": "Unisciti al gruppo",
"postNewBulletinLabel": "Pubblica un nuovo bollettino",
"titlePlaceholder": "titolo...",
"blocked": "Bloccato",
"search": "Ricerca...",
"copyBtn": "Copia",
"copiedClipboardNotification": "Copiato negli Appunti",
"newGroupBtn": "Crea un nuovo gruppo",
"invitationLabel": "Invito",
"serverInfo": "Informazioni sul server",
"membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.",
"couldNotSendMsgError": "Impossibile inviare questo messaggio",
"acknowledgedLabel": "Riconosciuto",
"pendingLabel": "In corso",
"acceptGroupInviteLabel": "Vuoi accettare l'invito a",
"acceptGroupBtn": "Accetta",
"rejectGroupBtn": "Rifiuta",
"chatBtn": "Chat",
"listsBtn": "Liste",
"bulletinsBtn": "Bollettini",
"addressLabel": "Indirizzo",
"displayNameLabel": "Nome visualizzato",
"saveBtn": "Salva",
"password": "Password",
"error0ProfilesLoadedForPassword": "0 profili caricati con quella password",
"unlock": "Sblocca",
"addNewItem": "Aggiungi un nuovo elemento alla lista",
"todoPlaceholder": "Da fare...",
"serverSynced": "Sincronizzato",
"inviteToGroupLabel": "Invitare nel gruppo",
"inviteBtn": "Invitare",
"deleteBtn": "Elimina",
"update": "Aggiornamento",
"addProfileTitle": "Aggiungi nuovo profilo",
"profileName": "Nome visualizzato",
"defaultProfileName": "Alice",
"radioUsePassword": "Password",
"radioNoPassword": "Non criptato (senza password)",
"password1Label": "Password",
"password2Label": "Reinserire la password",
"passwordErrorEmpty": "La password non può essere vuota",
"passwordErrorMatch": "Le password non corrispondono",
"passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata",
"deleteConfirmLabel": "Digita ELIMINA per confermare",
"deleteConfirmText": "ELIMINA",
"addNewProfileBtn": "Aggiungi nuovo profilo",
"enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili",
"cwtchSettingsTitle": "Impostazioni di Cwtch",
"zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)",
"settingLanguage": "Lingua",
"settingInterfaceZoom": "Livello di zoom",
"largeTextLabel": "Grande",
"settingTheme": "Tema",
"themeLight": "Chiaro",
"themeDark": "Scuro",
"defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:",
"smallTextLabel": "Piccolo",
"loadingTor": "Caricamento di tor...",
"networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione",
"networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor",
"networkStatusOnline": "Online"
}

View File

@ -1,200 +1,288 @@
{
"@@locale": "pl",
"@@last_modified": "2021-07-07T23:42:20+02:00",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
"settingUIColumnSingle": "Single",
"settingUIColumnLandscape": "UI Columns in Landscape Mode",
"settingUIColumnPortrait": "UI Columns in Portrait Mode",
"localePl": "Polish",
"tooltipRemoveThisQuotedMessage": "Remove quoted message.",
"tooltipReplyToThisMessage": "Reply to this message",
"tooltipRejectContactRequest": "Reject this contact request",
"tooltipAcceptContactRequest": "Accept this contact request.",
"notificationNewMessageFromGroup": "New message in a group!",
"notificationNewMessageFromPeer": "New message from a contact!",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",
"shutdownCwtchTooltip": "Shutdown Cwtch",
"malformedMessage": "Malformed message",
"profileDeleteSuccess": "Successfully deleted profile",
"debugLog": "Turn on console debug logging",
"torNetworkStatus": "Tor network status",
"addContactFirst": "Add or pick a contact to begin chatting.",
"createProfileToBegin": "Please create or unlock a profile to begin",
"nickChangeSuccess": "Profile nickname changed successfully",
"addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"sendMessage": "Send Message",
"cancel": "Cancel",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"@@last_modified": "2021-12-20T09:20:03+01:00",
"msgAddToAccept": "Dodaj tego użytkownika do znajomych aby pobrać plik.",
"btnSendFile": "Wyślij plik",
"msgConfirmSend": "Na pewno chcesz wysłać ten plik?",
"msgFileTooBig": "Rozmiar pliku nie może przekraczać 10 GB",
"storageMigrationModalMessage": "Migrowanie profili do nowego formatu. To może chwilę potrwać...",
"loadingCwtch": "Ładowanie Cwtch...",
"themeColorLabel": "Motyw",
"themeNameNeon2": "Neon 2",
"themeNameNeon1": "Neon 1",
"themeNameMidnight": "Północ",
"themeNameMermaid": "Syrena",
"themeNamePumpkin": "Dynia",
"themeNameGhost": "Duch",
"themeNameVampire": "Wampir",
"themeNameWitch": "Czarownica",
"themeNameCwtch": "Cwtch",
"settingDownloadFolder": "Folder dla pobranych plików",
"settingImagePreviewsDescription": "Automatyczne pobieranie i podgląd obrazów. Pamiętaj, że podgląd obrazów jest potencjalną luką w zabezpieczeniach i nie należy używać tej eksperymentalnej funkcjonalności jeśli używasz Cwtch do komunikacji z niezaufanymi osobami. Zdjęcia profilowe są przewidziane na wersję Cwtch 1.6",
"settingImagePreviews": "Podgląd zdjęć i zdjęcia profilowe",
"experimentClickableLinksDescription": "Klikalne linki (eksperymentalne). Umożliwia klikanie na linki w wiadomościach, aby je otworzyć",
"enableExperimentClickableLinks": "Włącz klikalne linki",
"serverConnectionsLabel": "Połączenie",
"serverTotalMessagesLabel": "Wiadomości łącznie",
"serverMetricsLabel": "Statystyki serwera",
"manageKnownServersShort": "Serwery",
"manageKnownServersLong": "Zarządzaj znanymi serwerami",
"displayNameTooltip": "Wprowadź nazwę",
"manageKnownServersButton": "Zarządzaj znanymi serwerami",
"fieldDescriptionLabel": "Opis",
"groupsOnThisServerLabel": "Grupy na tym serwerze, których jesteś członkiem",
"importLocalServerButton": "Importuj %1",
"importLocalServerSelectText": "Wybierz lokalny serwer",
"importLocalServerLabel": "Importuj lokalnie hostowany serwer",
"titleManageServers": "Zarządzaj serwerami",
"leaveGroup": "Opuść grupę",
"yesLeave": "Opuść",
"newPassword": "Nowe hasło",
"accepted": "Zaakceptowano!",
"rejected": "Odrzucono!",
"sendAnInvitation": "You sent an invitation for: ",
"torVersion": "Wersja Tor",
"torStatus": "Status Tor",
"resetTor": "Reset",
"cancel": "Anuluj",
"sendMessage": "Wyślij wiadomość",
"sendInvite": "Wyślij zaproszenie do znajomych albo do grupy",
"deleteProfileSuccess": "Profil został usunięty",
"nickChangeSuccess": "Nazwa profilu została zmieniona",
"torNetworkStatus": "Status sieci Tor",
"debugLog": "Włącz zapisywanie logów konsoli",
"profileDeleteSuccess": "Profil został usunięty",
"malformedMessage": "Wiadomość uszkodzona",
"shutdownCwtchTooltip": "Zamknij Cwtch",
"shutdownCwtchDialogTitle": "Zamknąć Cwtch?",
"shutdownCwtchAction": "Zamknij Cwtch",
"tooltipShowPassword": "Pokaż hasło",
"tooltipHidePassword": "Ukryj hasło",
"notificationNewMessageFromPeer": "Nowa wiadomość od znajomego!",
"notificationNewMessageFromGroup": "Nowa wiadomość w grupie!",
"tooltipAcceptContactRequest": "Akceptuj zaproszenie do znajomych",
"tooltipRejectContactRequest": "Odrzuć zaproszenie do znajomych",
"tooltipReplyToThisMessage": "Odpowiedz na tę wiadomość",
"tooltipRemoveThisQuotedMessage": "Usuń cytowaną wiadomość.",
"settingUIColumnDouble12Ratio": "Podwójny (1:2)",
"settingUIColumnSingle": "Pojedynczy",
"settingUIColumnDouble14Ratio": "Podwójny (1:4)",
"settingUIColumnOptionSame": "Tak samo jak w przypadku trybu wertykalnego",
"contactGoto": "Przejdź do konwersacji z %1",
"addContact": "Dodaj znajomego",
"addContactConfirm": "Dodaj znajomego %1",
"placeholderEnterMessage": "Wpisz wiadomość...",
"blockedMessageMessage": "Ta wiadomość jest od użytkownika, który został przez Ciebie zablokowany",
"showMessageButton": "Pokaż wiadomość",
"addServerTitle": "Dodaj serwer",
"editServerTitle": "Edytuj serwer",
"serverAddress": "Adres serwera",
"serverDescriptionLabel": "Opis serwera",
"serverEnabled": "Serwer włączony",
"serverEnabledDescription": "Uruchom lub zatrzymaj serwer",
"serverAutostartLabel": "Autostart",
"saveServerButton": "Zapisz serwer",
"serversManagerTitleLong": "Serwery, które hostujesz",
"serversManagerTitleShort": "Serwery",
"addServerTooltip": "Dodaj nowy serwer",
"enterServerPassword": "Wprowadź hasło, aby odblokować serwer",
"settingServers": "Hostowanie serwerów",
"enterCurrentPasswordForDeleteServer": "Wprowadź aktualne hasło, aby usunąć ten serwer",
"newMessagesLabel": "Nowe wiadomości",
"localePl": "Polski",
"localeRU": "Rosyjski",
"copyAddress": "Skopiuj adres",
"deleteServerSuccess": "Usunięto serwer",
"deleteServerConfirmBtn": "Usuń",
"fileSavedTo": "Zapisano do",
"fileCheckingStatus": "Sprawdzanie statusu pobierania",
"verfiyResumeButton": "Zweryfikuj\/wznów",
"copyServerKeys": "Kopiuj klucze",
"fileInterrupted": "Przerwano",
"encryptedServerDescription": "Zaszyfrowanie serwera hasłem chroni go przed dostępem innych osób korzystających z tego urządzenia.",
"plainServerDescription": "Zalecamy ustawienie hasła dla Cwtch. Jeśli hasło nie zostanie ustawione, każdy z dostępem do tego urządzenia uzyska dostęp do tego serwera, w tym do wrażliwych kluczy kryptograficznych.",
"settingServersDescription": "Hostowanie serwerów (eksperymentalne) umożliwia tworzenie i zarządzanie serwerami Cwtch",
"unlockProfileTip": "Utwórz albo odblokuj profil aby rozpocząć!",
"unlockServerTip": "Utwórz albo odblokuj serwer aby rozpocząć!",
"serverAutostartDescription": "Decyduje, czy aplikacja automatycznie włączy serwer po uruchomieniu",
"serverDescriptionDescription": "Opis serwera widoczny tylko dla Ciebie",
"blockUnknownConnectionsEnabledDescription": "Ta konwersacja zostanie usunięta gdy zamkniesz Cwtch! Możesz włączyć zapisywanie wiadomości dla każdej konwersacji osobno w menu w prawym górnym rogu.",
"archiveConversation": "Zarchiwizuj tę rozmowę",
"streamerModeLabel": "Tryb streamera\/prezentacji",
"retrievingManifestMessage": "Pobieranie informacji o pliku...",
"openFolderButton": "Otwórz folder",
"downloadFileButton": "Pobierz",
"labelFilename": "Nazwa pliku",
"labelFilesize": "Rozmiar",
"messageEnableFileSharing": "Włącz udostępnianie plików (eksperymentalne) aby wyświetlić tę wiadomość",
"messageFileSent": "Plik został wysłany",
"messageFileOffered": "Znajomy chce wysłać Ci plik",
"tooltipSendFile": "Wyślij plik",
"settingFileSharing": "Udostępnianie plików",
"descriptionFileSharing": "Udostępnianie plików (eksperymentalne) umożliwia wysyłanie i otrzymywanie plików od znajomych i grup. Pamiętaj, że udostępnienie pliku grupie spowoduje bezpośrednie połączenie się członków grupy z Tobą przez Cwtch w celu pobrania pliku.",
"titleManageProfilesShort": "Profil",
"descriptionStreamerMode": "Ukrywa wrażliwe elementy interfejsu, np. adresy profili i kontaktów, na potrzeby udostępniania swojego ekranu",
"plainProfileDescription": "Zalecamy ustawienie hasła dla profilu Cwtch. Jeśli hasło nie zostanie ustawione, każdy z dostępem do tego urządzenia uzyska dostęp do tego profilu, w tym do wiadomości, znajomych i wrażliwych kluczy kryptograficznych.",
"encryptedProfileDescription": "Zaszyfrowanie profilu hasłem chroni go przed dostępem innych osób korzystających z tego urządzenia",
"settingUIColumnLandscape": "Kolumny interfejsu w trybie horyzontalnym",
"settingUIColumnPortrait": "Kolumny interfejsu w trybie wertykalnym",
"groupInviteSettingsWarning": "Zaproszono Cię do grupy! Aby wyświetlić to zaproszenie, włącz Czaty Grupowe (eksperymentalne) w Ustawieniach",
"shutdownCwtchDialog": "Zamknąć Cwtch? Wszystkie połączenia zostaną zakończone, a aplikacja zostanie zamknięta.",
"addContactFirst": "Wybierz lub dodaj znajomego aby rozpocząć konwersację.",
"createProfileToBegin": "Utwórz albo odblokuj profil aby rozpocząć",
"addServerFirst": "Musisz dodać serwer, aby utworzyć grupę",
"contactSuggestion": "This is a contact suggestion for: ",
"rejected": "Rejected!",
"accepted": "Accepted!",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateNever": "Never",
"dateLastYear": "Last Year",
"dateYesterday": "Yesterday",
"dateLastMonth": "Last Month",
"dateRightNow": "Right Now",
"successfullAddedContact": "Successfully added ",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"tooltipOpenSettings": "Open the settings pane",
"chatHistoryDefault": "Ta konwersacja zostanie usunięta gdy zamkniesz Cwtch! Możesz włączyć zapisywanie wiadomości dla każdej konwersacji osobno w menu w prawym górnym rogu.",
"reallyLeaveThisGroupPrompt": "Na pewno chcesz opuścić tę grupę? Wszystkie wiadomości i atrybuty zostaną usunięte.",
"inviteToGroup": "Zaproszono Cię do grupy:",
"dateNever": "Nigdy",
"dateLastYear": "Rok temu",
"dateYesterday": "Wczoraj",
"dateLastMonth": "Miesiąc temu",
"dateRightNow": "Teraz",
"successfullAddedContact": "Dodano znajomego ",
"descriptionBlockUnknownConnections": "Blokowanie połączeń od osób, które nie są na liście Twoich znajomych.",
"descriptionExperimentsGroups": "Czaty grupowe (eksperymentalne) łączą się z niezaufanymi serwerami, aby umożliwić komunikację grupową.",
"descriptionExperiments": "Funkcje eksperymentalne są opcjonalne. Dodają one funkcjonalności, które mogą być mniej prywatne niż domyślne konwersacje 1:1, np. czaty grupowe, integracja z botami, itp.",
"titleManageProfiles": "Zarządzaj Profilami",
"tooltipUnlockProfiles": "Wprowadź hasło, aby odblokować zaszyfrowane profile.",
"titleManageContacts": "Konwersacje",
"tooltipAddContact": "Dodaj znajomego lub grupę",
"tooltipOpenSettings": "Ustawienia",
"contactAlreadyExists": "Ten znajomy już istnieje",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Enable Experiments",
"conversationSettings": "Ustawienia konwersacji",
"enterCurrentPasswordForDelete": "Aby usunąć ten profil, wprowadź hasło.",
"enableGroups": "Włącz czaty grupowe",
"localeIt": "Italiana",
"localeEs": "Espanol",
"todoPlaceholder": "Do zdobienia...",
"addNewItem": "Dodaj do listy",
"addListItem": "Add a New List Item",
"addNewItem": "Add a new item to the list",
"todoPlaceholder": "Todo...",
"newConnectionPaneTitle": "New Connection",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Connecting to network and peers...",
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
"viewGroupMembershipTooltip": "View Group Membership",
"loadingTor": "Loading tor...",
"smallTextLabel": "Small",
"defaultScalingText": "Default size text (scale factor:",
"builddate": "Built on: %2",
"version": "Version %1",
"versionTor": "Version %1 with tor %2",
"themeDark": "Dark",
"themeLight": "Light",
"settingTheme": "Theme",
"largeTextLabel": "Large",
"settingInterfaceZoom": "Zoom level",
"newConnectionPaneTitle": "Nowe połączenie",
"networkStatusOnline": "Połączono",
"networkStatusConnecting": "Łączenie z siecią i znajomymi...",
"networkStatusAttemptingTor": "Próba połączenia z siecią Tor",
"networkStatusDisconnected": "Rozłączono. Sprawdź połączenie z Internetem",
"viewGroupMembershipTooltip": "Wyświetl członków grupy",
"loadingTor": "Ładowanie Tor...",
"smallTextLabel": "Mały",
"defaultScalingText": "Domyślny rozmiar tekstu (skalowanie:",
"builddate": "Zbudowana: %2",
"version": "Wersja %1",
"versionTor": "Wersja %1 , wersja Tor %2",
"experimentsEnabled": "Włącz funkcje eksperymentalne",
"themeDark": "Ciemny",
"themeLight": "Jasny",
"settingTheme": "Motyw",
"largeTextLabel": "Duży",
"settingInterfaceZoom": "Przybliżenie",
"localeDe": "Deutsche",
"localePt": "Portuguesa",
"localeFr": "Frances",
"localeEn": "English",
"settingLanguage": "Language",
"blockUnknownLabel": "Block Unknown Peers",
"zoomLabel": "Interface zoom (mostly affects text and button sizes)",
"versionBuilddate": "Version: %1 Built on: %2",
"cwtchSettingsTitle": "Cwtch Settings",
"unlock": "Unlock",
"yourServers": "Your Servers",
"yourProfiles": "Your Profiles",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
"password": "Password",
"enterProfilePassword": "Enter a password to view your profiles",
"addNewProfileBtn": "Add new profile",
"deleteConfirmText": "DELETE",
"deleteProfileConfirmBtn": "Really Delete Profile",
"deleteConfirmLabel": "Type DELETE to confirm",
"deleteProfileBtn": "Delete Profile",
"passwordChangeError": "Error changing password: Supplied password rejected",
"passwordErrorMatch": "Passwords do not match",
"saveProfileBtn": "Save Profile",
"createProfileBtn": "Create Profile",
"passwordErrorEmpty": "Password cannot be empty",
"password2Label": "Reenter password",
"password1Label": "Password",
"currentPasswordLabel": "Current Password",
"yourDisplayName": "Your Display Name",
"profileOnionLabel": "Send this address to peers you want to connect with",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copied to Clipboard",
"copyBtn": "Copy",
"editProfile": "Edit Profille",
"newProfile": "New Profile",
"defaultProfileName": "Alice",
"profileName": "Display name",
"editProfileTitle": "Edit Profile",
"addProfileTitle": "Add new profile",
"deleteBtn": "Delete",
"unblockBtn": "Unblock Peer",
"dontSavePeerHistory": "Delete Peer History",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
"savePeerHistory": "Save Peer History",
"blockBtn": "Block Peer",
"saveBtn": "Save",
"displayNameLabel": "Display Name",
"addressLabel": "Address",
"puzzleGameBtn": "Puzzle Game",
"bulletinsBtn": "Bulletins",
"listsBtn": "Lists",
"settingLanguage": "Język",
"blockUnknownLabel": "Blokuj nieznajomych",
"zoomLabel": "Przybliżenie interfejsu (wpływa głównie na rozmiar tekstu i przycisków)",
"versionBuilddate": "Wersja: %1 Zbudowana: %2",
"cwtchSettingsTitle": "Ustawienia Cwtch",
"unlock": "Odblokuj",
"yourServers": "Twoje serwery",
"yourProfiles": "Twoje profile",
"error0ProfilesLoadedForPassword": "Znaleziono 0 profili z tym hasłem",
"password": "Hasło",
"enterProfilePassword": "Wprowadź hasło aby wyświetlić profile",
"addNewProfileBtn": "Dodaj",
"deleteConfirmText": "USUŃ",
"deleteProfileConfirmBtn": "Usuń",
"deleteConfirmLabel": "Wpisz USUŃ aby potwierdzić",
"deleteProfileBtn": "Usuń profil",
"passwordChangeError": "Zmiana hasła nie powiodła się: hasło niepoprawne",
"passwordErrorMatch": "Hasła nie są identyczne",
"saveProfileBtn": "Zapisz profil",
"createProfileBtn": "Utwórz profil",
"passwordErrorEmpty": "Hasło nie może być puste",
"password2Label": "Powtórz hasło",
"password1Label": "Hasło",
"currentPasswordLabel": "Obecne hasło",
"yourDisplayName": "Nazwa",
"profileOnionLabel": "Przekaż ten adres znajomym, z którymi chcesz się połączyć",
"noPasswordWarning": "Brak hasła do konta oznacza, że dane przechowywane na tym urządzeniu nie będą zaszyfrowane",
"radioNoPassword": "Niezaszyfrowany (brak hasła)",
"radioUsePassword": "Hasło",
"copiedToClipboardNotification": "Skopiowano do schowka",
"editProfile": "Edytuj profil",
"newProfile": "Nowy profil",
"defaultProfileName": "Nowy profil",
"profileName": "Nazwa",
"editProfileTitle": "Edytuj profil",
"addProfileTitle": "Dodaj nowy profil",
"deleteBtn": "Usuń",
"unblockBtn": "Odblokuj",
"dontSavePeerHistory": "Nie",
"savePeerHistoryDescription": "Zapisywanie wiadomości",
"savePeerHistory": "Tak",
"blockBtn": "Zablokuj",
"saveBtn": "Zapisz",
"displayNameLabel": "Nazwa",
"addressLabel": "Adresy",
"puzzleGameBtn": "Puzzle",
"bulletinsBtn": "Biuletyny",
"listsBtn": "Listy",
"chatBtn": "Chat",
"rejectGroupBtn": "Reject",
"acceptGroupBtn": "Accept",
"acceptGroupInviteLabel": "Do you want to accept the invitation to",
"newGroupBtn": "Create new group",
"copiedClipboardNotification": "Copied to clipboard",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
"peerBlockedMessage": "Peer is blocked",
"pendingLabel": "Pending",
"acknowledgedLabel": "Acknowledged",
"couldNotSendMsgError": "Could not send this message",
"dmTooltip": "Click to DM",
"membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.",
"addListItemBtn": "Add Item",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
"rejectGroupBtn": "Odrzuć",
"acceptGroupBtn": "Akceptuj",
"acceptGroupInviteLabel": "Czy chcesz zaakceptować zaproszenie do grupy",
"newGroupBtn": "Utwórz nową grupę",
"copiedClipboardNotification": "Skopiowano do schowka",
"copyBtn": "Kopiuj",
"peerOfflineMessage": "Znajomy jest niedostępny, nie można dostarczyć wiadomości",
"peerBlockedMessage": "Użytkownik jest zablokowany",
"pendingLabel": "W toku",
"acknowledgedLabel": "OK",
"couldNotSendMsgError": "Wiadomość nie została wysłana",
"dmTooltip": "Kliknij, aby wysłać wiadomość",
"membershipDescription": "Lista użytkowników, którzy wysyłali wiadomości w tej grupie. Członkowie grupy, którzy nie wysyłali żadnych wiadomości nie są na tej liście.",
"addListItemBtn": "Dodaj",
"peerNotOnline": "Znajomy jest niedostępny. Nie można użyć aplikacji.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Invite",
"inviteToGroupLabel": "Invite to group",
"groupNameLabel": "Group Name",
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected",
"serverInfo": "Server Information",
"invitationLabel": "Invitation",
"serverLabel": "Server",
"search": "Search...",
"update": "Zaktualizuj",
"inviteBtn": "Zaproś",
"inviteToGroupLabel": "Zaproś do grupy",
"groupNameLabel": "Nazwa grupy",
"viewServerInfo": "Informacje o serwerze",
"serverNotSynced": "Synchronizacja wiadomości (to może chwilę potrwać)...",
"serverSynced": "Zsynchronizowano",
"serverConnectivityDisconnected": "Brak połączenia z serwerem",
"serverConnectivityConnected": "Połączono z serwerem",
"serverInfo": "Informacje o serwerze",
"invitationLabel": "Zaproszenie",
"serverLabel": "Serwer",
"search": "Szukaj...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked",
"titlePlaceholder": "title...",
"postNewBulletinLabel": "Post new bulletin",
"newBulletinLabel": "New Bulletin",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
"groupName": "Group name",
"peerName": "Name",
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Create",
"defaultGroupName": "Awesome Group",
"createGroupTitle": "Create Group"
}
"blocked": "Zablokowany",
"pasteAddressToAddContact": "Wklej adres Cwtch znajomego, zaproszenie do grupy albo pęk kluczy",
"titlePlaceholder": "Tytuł...",
"postNewBulletinLabel": "Opublikuj nowy biuletyn",
"newBulletinLabel": "Nowy biuletyn",
"joinGroup": "Dołącz do grupy",
"createGroup": "Utwórz grupę",
"addPeer": "Dodaj znajomego",
"groupAddr": "Adres",
"invitation": "Zaproszenie",
"server": "Serwer",
"groupName": "Nazwa grupy",
"peerName": "Nazwa",
"peerAddress": "Adres",
"joinGroupTab": "Dołącz do grupy",
"createGroupTab": "Utwórz grupę",
"addPeerTab": "Dodaj znajomego",
"createGroupBtn": "Utwórz",
"defaultGroupName": "Nowa grupa",
"createGroupTitle": "Utwórz grupę"
}

View File

@ -1,6 +1,94 @@
{
"@@locale": "pt",
"@@last_modified": "2021-07-07T23:42:20+02:00",
"@@last_modified": "2021-12-20T09:20:03+01:00",
"msgAddToAccept": "Add this account to your contacts in order to accept this file.",
"btnSendFile": "Send File",
"msgConfirmSend": "Are you sure you want to send",
"msgFileTooBig": "File size cannot exceed 10 GB",
"storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...",
"loadingCwtch": "Loading Cwtch...",
"themeColorLabel": "Color Theme",
"themeNameNeon2": "Neon2",
"themeNameNeon1": "Neon1",
"themeNameMidnight": "Midnight",
"themeNameMermaid": "Mermaid",
"themeNamePumpkin": "Pumpkin",
"themeNameGhost": "Ghost",
"themeNameVampire": "Vampire",
"themeNameWitch": "Witch",
"themeNameCwtch": "Cwtch",
"settingDownloadFolder": "Download Folder",
"settingImagePreviewsDescription": "Images will be downloaded and previewed automatically. Please note that image previews can often lead to security vulnerabilities, and you should not enable this Experiment if you use Cwtch with untrusted contacts. Profile pictures are planned for Cwtch 1.6.",
"settingImagePreviews": "Image Previews and Profile Pictures",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"newMessagesLabel": "New Messages",
"localeRU": "Russian",
"copyServerKeys": "Copy keys",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
"deleteServerConfirmBtn": "Really delete server",
"deleteServerSuccess": "Successfully deleted server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
"messageFileOffered": "Contact is offering to send you a file",
"messageFileSent": "You sent a file",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size",
"labelFilename": "Filename",
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
"plainProfileDescription": "We recommend that you protect your Cwtch profiles with a password. If you do not set a password on this profile then anyone who has access to this device may be able to access information about this profile, including contacts, messages and sensitive cryptographic keys.",
"encryptedProfileDescription": "Encrypting a profile with a password protects it from other people who may also use this device. Encrypted profiles cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
@ -16,7 +104,6 @@
"notificationNewMessageFromPeer": "New message from a contact!",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
@ -47,9 +134,6 @@
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateNever": "Never",
"dateLastYear": "Last Year",
@ -62,21 +146,22 @@
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"titleManageContacts": "Conversations",
"tooltipAddContact": "Add a new contact or conversation",
"tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"invalidImportString": "Invalid import string",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Enable Experiments",
"localeIt": "Italiana",
"localeEs": "Espanol",
"addListItem": "Adicionar Item à Lista",
"addNewItem": "Adicionar novo item à lista",
"todoPlaceholder": "Afazer…",
"addNewItem": "Adicionar novo item à lista",
"addListItem": "Adicionar Item à Lista",
"newConnectionPaneTitle": "New Connection",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Connecting to network and peers...",
"networkStatusConnecting": "Connecting to network and contacts...",
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
"viewGroupMembershipTooltip": "View Group Membership",
@ -86,6 +171,7 @@
"builddate": "Built on: %2",
"version": "Version %1",
"versionTor": "Version %1 with tor %2",
"experimentsEnabled": "Enable Experiments",
"themeDark": "Dark",
"themeLight": "Light",
"settingTheme": "Theme",
@ -96,7 +182,7 @@
"localeFr": "Frances",
"localeEn": "English",
"settingLanguage": "Language",
"blockUnknownLabel": "Block Unknown Peers",
"blockUnknownLabel": "Block Unknown Contacts",
"zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)",
"versionBuilddate": "Version: %1 Built on: %2",
"cwtchSettingsTitle": "Configurações do Cwtch",
@ -120,12 +206,11 @@
"password1Label": "Password",
"currentPasswordLabel": "Current Password",
"yourDisplayName": "Your Display Name",
"profileOnionLabel": "Send this address to peers you want to connect with",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copiado",
"copyBtn": "Copiar",
"editProfile": "Edit Profille",
"newProfile": "New Profile",
"defaultProfileName": "Alice",
@ -133,11 +218,11 @@
"editProfileTitle": "Edit Profile",
"addProfileTitle": "Add new profile",
"deleteBtn": "Deletar",
"unblockBtn": "Unblock Peer",
"dontSavePeerHistory": "Delete Peer History",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
"savePeerHistory": "Save Peer History",
"blockBtn": "Block Peer",
"unblockBtn": "Unblock Contact",
"dontSavePeerHistory": "Delete History",
"savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.",
"savePeerHistory": "Save History",
"blockBtn": "Block Contact",
"saveBtn": "Salvar",
"displayNameLabel": "Nome de Exibição",
"addressLabel": "Endereço",
@ -150,21 +235,23 @@
"acceptGroupInviteLabel": "Você quer aceitar o convite para",
"newGroupBtn": "Criar novo grupo",
"copiedClipboardNotification": "Copiado",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
"peerBlockedMessage": "Peer is blocked",
"copyBtn": "Copiar",
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
"peerBlockedMessage": "Contact is blocked",
"pendingLabel": "Pendente",
"acknowledgedLabel": "Confirmada",
"couldNotSendMsgError": "Não deu para enviar esta mensagem",
"dmTooltip": "Clique para DM",
"membershipDescription": "A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo.",
"addListItemBtn": "Add Item",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Convidar",
"inviteToGroupLabel": "Convidar ao grupo",
"groupNameLabel": "Nome do Grupo",
"groupNameLabel": "Nome do grupo",
"viewServerInfo": "Server Info",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected",
@ -179,12 +266,13 @@
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked",
"pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…",
"titlePlaceholder": "título…",
"postNewBulletinLabel": "Postar novo boletim",
"newBulletinLabel": "Novo Boletim",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"addPeer": "Add Contact",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
@ -193,7 +281,7 @@
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"addPeerTab": "Add a contact",
"createGroupBtn": "Criar",
"defaultGroupName": "Grupo incrível",
"createGroupTitle": "Criar Grupo"

288
lib/l10n/intl_ru.arb Normal file
View File

@ -0,0 +1,288 @@
{
"@@locale": "ru",
"@@last_modified": "2021-12-20T09:20:03+01:00",
"msgAddToAccept": "Add this account to your contacts in order to accept this file.",
"btnSendFile": "Send File",
"msgConfirmSend": "Are you sure you want to send",
"msgFileTooBig": "File size cannot exceed 10 GB",
"storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...",
"loadingCwtch": "Loading Cwtch...",
"themeColorLabel": "Тема",
"themeNameNeon2": "Неон2",
"themeNameNeon1": "Неон1",
"themeNameMidnight": "Полночь",
"themeNameMermaid": "Русалка",
"themeNamePumpkin": "Тыква",
"themeNameGhost": "Призрак",
"themeNameVampire": "Вампир",
"themeNameWitch": "Ведьма",
"themeNameCwtch": "Cwtch",
"settingDownloadFolder": "Скачать папку",
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами. Аватары профиля запланированы для версии Cwtch 1.6.",
"settingImagePreviews": "Предпросмотр изображений и фотографий профиля",
"experimentClickableLinksDescription": "Экспериментальная функция которая позволяет нажимать на URL адреса в сообщениях",
"enableExperimentClickableLinks": "Включить кликабельные ссылки",
"serverConnectionsLabel": "Соединение",
"serverTotalMessagesLabel": "Всего сообщений",
"serverMetricsLabel": "Показатели сервера",
"manageKnownServersShort": "Серверы",
"manageKnownServersLong": "Управление серверами",
"displayNameTooltip": "Введите отображаемое имя",
"manageKnownServersButton": "Управление серверами",
"fieldDescriptionLabel": "Описание",
"groupsOnThisServerLabel": "Группы, в которых я нахожусь, размещены на этом сервере",
"importLocalServerButton": "Импорт %1",
"importLocalServerSelectText": "Выбрать локальный сервер",
"importLocalServerLabel": "Импортировать локальный сервер",
"newMessagesLabel": "Новое сообщение",
"localeRU": "Русский",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"savePeerHistory": "Хранить историю",
"saveBtn": "Сохранить",
"networkStatusOnline": "В сети",
"copiedToClipboardNotification": "Скопировано в буфер обмена",
"defaultProfileName": "Алиса",
"deleteBtn": "Удалить",
"bulletinsBtn": "Бюллетень",
"groupNameLabel": "Имя группы",
"serverLabel": "Сервер",
"copyBtn": "Копировать",
"copyServerKeys": "Копировать ключи",
"verfiyResumeButton": "Проверить\/продолжить",
"fileCheckingStatus": "Проверка статуса загрузки",
"fileInterrupted": "Прервано",
"fileSavedTo": "Сохранить в",
"encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.",
"plainServerDescription": "Мы настоятельно рекомендуем защитить свой сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.",
"deleteServerConfirmBtn": "Точно удалить сервер?",
"deleteServerSuccess": "Сервер успешно удален",
"enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его",
"copyAddress": "Копировать адрес",
"settingServersDescription": "Экспериментальная функция которая позволяет добавлять сервер Cwtch. В меню появится дополнительная опция Серверы",
"settingServers": "Использовать серверы",
"enterServerPassword": "Введите пароль для разблокировки сервера",
"unlockProfileTip": "Создайте или разблокируйте профиль, чтобы начать",
"unlockServerTip": "Создайте или разблокируйте сервер, чтобы начать",
"addServerTooltip": "Добавить сервер",
"serversManagerTitleShort": "Серверы",
"serversManagerTitleLong": "Личные серверы",
"saveServerButton": "Сохранить сервер",
"serverAutostartDescription": "Автозапуск сервера при старте программы",
"serverAutostartLabel": "Автозапуск",
"serverEnabledDescription": "Запустить или остановить сервер",
"serverEnabled": "Сервер запущен",
"serverDescriptionDescription": "Описание видите только Вы. Сделано для удобства",
"serverDescriptionLabel": "Описание сервера",
"serverAddress": "Адрес сервера",
"editServerTitle": "Изменить сервер",
"addServerTitle": "Добавить сервер",
"titleManageProfilesShort": "Профили",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch.",
"settingFileSharing": "Передача файлов",
"tooltipSendFile": "Отправить файл",
"messageFileOffered": "Контакт предлагает загрузить вам файл",
"messageFileSent": "Вы отправили файл",
"messageEnableFileSharing": "Включите экспериментальную функцию Обмен файлами чтобы просмотреть это сообщение.",
"labelFilesize": "Размер",
"labelFilename": "Имя-файла",
"downloadFileButton": "Загрузить",
"openFolderButton": "Открыть папку",
"retrievingManifestMessage": "Получение информации о файле...",
"descriptionStreamerMode": "При включении этого параметра, внешний вид некоторых элементов становится более приватным, скрывая длинные Onion адреса и адреса контактов, оставляя только заданные имена",
"streamerModeLabel": "Режим презентации",
"archiveConversation": "Отправить чат в архив",
"blockUnknownConnectionsEnabledDescription": "Соединения от неизвестных контактов блокируются. Данный параметр можно изменить в настройках",
"showMessageButton": "Показать сообщения",
"blockedMessageMessage": "Это сообщение из заблокированного вами профиля.",
"placeholderEnterMessage": "Написать сообщение...",
"plainProfileDescription": "Мы рекомендуем защитить свой ПРОФИЛЬ Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к устройству, сможет получить доступ к информации об этом профиле, включая контакты, сообщения и конфиденциальные криптографические ключи.",
"encryptedProfileDescription": "Шифрование ПРОФИЛЯ паролем защитит его от других людей у которых может оказаться доступ к этому устройству. Зашифрованный ПРОФИЛЬ нельзя расшифровать, пока не будет введен правильный пароль разблокировки.",
"addContactConfirm": "Добавить контакт %1",
"addContact": "Добавить контакт",
"contactGoto": "Перейти к сообщению от %1",
"settingUIColumnOptionSame": "Как в настройках портретного режима",
"settingUIColumnDouble14Ratio": "Двойной (1:4)",
"settingUIColumnDouble12Ratio": "Двойной (1:2)",
"settingUIColumnSingle": "Одиночный",
"settingUIColumnLandscape": "UI столбцы в Ландшафтном Режиме",
"settingUIColumnPortrait": "UI столбцы в Портретном режиме",
"localePl": "Польский",
"tooltipRemoveThisQuotedMessage": "Удалить цитируемое сообщение.",
"tooltipReplyToThisMessage": "Ответить на это сообщение",
"tooltipRejectContactRequest": "Отклонить запрос в контакты.",
"tooltipAcceptContactRequest": "Принять запрос в контакты.",
"notificationNewMessageFromGroup": "Новое сообщение в группе!",
"notificationNewMessageFromPeer": "Новое сообщение от контакта!",
"tooltipHidePassword": "Скрыть пароль",
"tooltipShowPassword": "Показать пароль",
"groupInviteSettingsWarning": "Вас пригласили присоединиться к группе! Пожалуйста, включите экспериментальную функцию групповые чаты в Настройках, чтобы просмотреть это приглашение.",
"shutdownCwtchAction": "Выключить Cwtch",
"shutdownCwtchDialog": "Вы уверены, что хотите выключить Cwtch? Это приведет к закрытию всех подключений и выходу из приложения.",
"shutdownCwtchDialogTitle": "Выключить Cwtch?",
"shutdownCwtchTooltip": "Выключить Cwtch",
"malformedMessage": "Некорректное сообщение",
"profileDeleteSuccess": "Профиль успешно удален",
"debugLog": "Влючить отладку через консоль",
"torNetworkStatus": "Статус сети Tor",
"addContactFirst": "Добавьте или выберите контакт, чтобы начать чат.",
"createProfileToBegin": "Пожалуйста, создайте или разблокируйте профиль, чтобы начать",
"nickChangeSuccess": "Имя профиля успешно изменено",
"addServerFirst": "Перед созданием группы, необходимо создать сервер",
"deleteProfileSuccess": "Профиль успешно удален",
"sendInvite": "Отправить контакт или приглашение в группу",
"sendMessage": "Отправить сообщение",
"cancel": "Отмена",
"resetTor": "Сбросс",
"torStatus": "Статус Tor",
"torVersion": "Версия Tor",
"sendAnInvitation": "Вы отправили приглашение для: ",
"contactSuggestion": "Вам предложили этот контакт: ",
"rejected": "Отклонить!",
"accepted": "Принять!",
"chatHistoryDefault": "Этот чат будет удален после закрытия Cwtch! Историю сообщений можно включить для каждого чата отдельно через меню настроек в правом верхнем углу..",
"newPassword": "Новый пароль",
"yesLeave": "Да, оставить этот чат",
"reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.",
"leaveGroup": "Да, оставить этот чат",
"inviteToGroup": "Вас пригласили присоединиться к группе:",
"titleManageServers": "Управление серверами",
"dateNever": "Никогда",
"dateLastYear": "Прошлый год",
"dateYesterday": "Вчера",
"dateLastMonth": "Прошлый месяц",
"dateRightNow": "Прямо сейчас",
"successfullAddedContact": "Успешно добавлен",
"descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей не состоящих в ваших контактах будут отклонены.",
"descriptionExperimentsGroups": "Данная экспериментальная функция позволяет Cwtch подключаться к недоверенной серверной инфраструктуре, чтобы облегчить Вам общение с более чем одним контактом.",
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный част 1 на 1..",
"titleManageProfiles": "Управление профилями Cwtch",
"tooltipUnlockProfiles": "Разблокировать зашифрованные профили, введя их пароль.",
"titleManageContacts": "Разговоры",
"tooltipAddContact": "Добавление нового контакта или разговора",
"tooltipOpenSettings": "Откройте панель настроек",
"contactAlreadyExists": "Контакт уже существует",
"invalidImportString": "Недействительная строка импорта",
"conversationSettings": "Настройки чата",
"enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.",
"enableGroups": "Включить Групповые чаты",
"localeIt": "Итальянский",
"localeEs": "Испанский",
"todoPlaceholder": "Выполняю...",
"addNewItem": "Добавить новый элемент в список",
"addListItem": "Добавить новый элемент",
"newConnectionPaneTitle": "Новое соединение",
"networkStatusConnecting": "Подключение к сети и контактам...",
"networkStatusAttemptingTor": "Попытка подключиться к сети Tor",
"networkStatusDisconnected": "Нет сети. Проверьте подключение к интернету",
"viewGroupMembershipTooltip": "Просмотр членства в группе",
"loadingTor": "Загрузка Tor...",
"smallTextLabel": "Маленький",
"defaultScalingText": "Размер текста по умолчанию (коэффициент масштабирования:",
"builddate": "Построен на: %2",
"version": "Версия %1",
"versionTor": "Версия %1 c tor %2",
"experimentsEnabled": "Включить Экспериментальные функции",
"themeDark": "Темная",
"themeLight": "Светлая",
"settingTheme": "Тема",
"largeTextLabel": "Большой",
"settingInterfaceZoom": "Уровень масштабирования",
"localeDe": "Немецкий",
"localePt": "Португальский",
"localeFr": "Французский",
"localeEn": "Английский",
"settingLanguage": "Язык",
"blockUnknownLabel": "Блокировать неизвестные контакты",
"zoomLabel": "Масштаб интерфейса (в основном влияет на размеры текста и кнопок)",
"versionBuilddate": "Версия: %1 Сборка от: %2",
"cwtchSettingsTitle": "Настройки Cwtch",
"unlock": "Разблокировать",
"yourServers": "Ваши Серверы",
"yourProfiles": "Ваши Профили",
"error0ProfilesLoadedForPassword": "0 профилей, загруженных с этим паролем",
"password": "Пароль",
"enterProfilePassword": "Введите пароль для просмотра ваших профилей",
"addNewProfileBtn": "Добавить новый профиль",
"deleteConfirmText": "УДАЛИТЬ",
"deleteProfileConfirmBtn": "Действительно удалить профиль?",
"deleteConfirmLabel": "Введите DELETE чтобы продолжить",
"deleteProfileBtn": "Удалить профиль",
"passwordChangeError": "Ошибка при смене пароля: Введенный пароль отклонен",
"passwordErrorMatch": "Пароли не совпадают",
"saveProfileBtn": "Сохранить профиль",
"createProfileBtn": "Создать профиль",
"passwordErrorEmpty": "Пароль не может быть пустым",
"password2Label": "Повторный ввод пароля",
"password1Label": "Пароль",
"currentPasswordLabel": "Текущий пароль",
"yourDisplayName": "Отображаемое имя",
"noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы",
"radioNoPassword": "Незашифрованный (без пароля)",
"radioUsePassword": "Пароль",
"editProfile": "Изменить профиль",
"newProfile": "Новый профиль",
"profileName": "Отображаемое имя",
"editProfileTitle": "Изменить профиль",
"addProfileTitle": "Добавить новый профиль",
"unblockBtn": "Разблокировать контакт",
"dontSavePeerHistory": "Удалить историю",
"savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.",
"blockBtn": "Заблокировать контакт",
"displayNameLabel": "Отображаемое имя",
"addressLabel": "Адрес",
"puzzleGameBtn": "Puzzle Game",
"listsBtn": "Списки",
"chatBtn": "Чат",
"rejectGroupBtn": "Отклонить",
"acceptGroupBtn": "Принять",
"acceptGroupInviteLabel": "Хотите принять приглашение в",
"newGroupBtn": "Создать новую группу",
"copiedClipboardNotification": "Скопировано в буфер обмена",
"peerOfflineMessage": "Контакт не в сети, сообщения не могут быть отправлены",
"peerBlockedMessage": "Контакт заблокирован",
"pendingLabel": "Ожидаемый",
"acknowledgedLabel": "Отправлено",
"couldNotSendMsgError": "Не удалось отправить это сообщение",
"dmTooltip": "Нажмите, чтобы перейти в DM",
"membershipDescription": "Ниже приведен список пользователей, отправивших сообщения группе. Этот список может не отражать всех пользователей, имеющих доступ к группе.",
"addListItemBtn": "Добавить элемент",
"peerNotOnline": "Контакт не в сети. Вы не можете связаться с ним пока он не появиться в сети.",
"searchList": "Список поиска",
"update": "Обновить",
"inviteBtn": "Пригласить",
"inviteToGroupLabel": "Пригласить в группу",
"viewServerInfo": "Информация о сервере",
"serverNotSynced": "Синхронизация новых сообщений (это может занять некоторое время)...",
"serverSynced": "Синхронизировано",
"serverConnectivityDisconnected": "Сервер отключен",
"serverConnectivityConnected": "Сервер подключен",
"serverInfo": "Информация о сервере",
"invitationLabel": "Приглашение",
"search": "Поиск...",
"cycleColoursDesktop": "Нажмите, чтобы переключать цвета.\nПравый клик чтобы сбросить.",
"cycleColoursAndroid": "Нажмите, чтобы переключать цвета.\nНажмите и удерживайте, чтобы сбросить.",
"cycleMorphsDesktop": "Нажмите, чтобы просмотреть формы.\nПравый клик чтобы сбросить.",
"cycleMorphsAndroid": "Нажмите, чтобы просмотреть формы.\nНажмите и удерживайте, чтобы сбросить.",
"cycleCatsDesktop": "Нажмите, чтобы просмотреть категории.\nПравый клик чтобы сбросить.",
"cycleCatsAndroid": "Нажмите, чтобы просмотреть категории.\nНажмите и удерживайте, чтобы сбросить.",
"blocked": "Заблокировано",
"pasteAddressToAddContact": "Вставьте адрес cwtch, приглашение или пакет ключей здесь, чтобы добавить их в контакты",
"titlePlaceholder": "заговолок...",
"postNewBulletinLabel": "Опубликовать новый бюллетень",
"newBulletinLabel": "Новый бюллетень",
"joinGroup": "Вступить в группу",
"createGroup": "Создать группу",
"addPeer": "Добавить контакт",
"groupAddr": "Адрес",
"invitation": "Приглашение",
"server": "Сервер",
"groupName": "Имя группы",
"peerName": "Имя",
"peerAddress": "Адрес",
"joinGroupTab": "Присоединиться к группе",
"createGroupTab": "Создать группу",
"addPeerTab": "Добавить контакт",
"createGroupBtn": "Создать",
"defaultGroupName": "Замечательная группа",
"createGroupTitle": "Создать группу"
}

View File

@ -1,7 +1,8 @@
import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:cwtch/notification_manager.dart';
import 'package:cwtch/themes/cwtch.dart';
import 'package:cwtch/views/messageview.dart';
import 'package:cwtch/widgets/rightshiftfixer.dart';
import 'package:flutter/foundation.dart';
import 'package:cwtch/cwtch/ffi.dart';
import 'package:cwtch/cwtch/gomobile.dart';
@ -9,26 +10,27 @@ import 'package:flutter/material.dart';
import 'package:cwtch/errorHandler.dart';
import 'package:cwtch/settings.dart';
import 'package:cwtch/torstatus.dart';
import 'package:cwtch/views/triplecolview.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'cwtch/cwtch.dart';
import 'cwtch/cwtchNotifier.dart';
import 'licenses.dart';
import 'model.dart';
import 'models/servers.dart';
import 'views/profilemgrview.dart';
import 'views/splashView.dart';
import 'dart:io' show Platform, exit;
import 'opaque.dart';
import 'themes/opaque.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var globalSettings = Settings(Locale("en", ''), OpaqueDark());
var globalSettings = Settings(Locale("en", ''), CwtchDark());
var globalErrorHandler = ErrorHandler();
var globalTorStatus = TorStatus();
var globalAppState = AppState();
var globalServersList = ServerListState();
void main() {
print("main()");
print("Cwtch version: ${EnvironmentConfig.BUILD_VER} built on: ${EnvironmentConfig.BUILD_DATE}");
LicenseRegistry.addLicense(() => licenses());
WidgetsFlutterBinding.ensureInitialized();
print("runApp()");
@ -61,13 +63,13 @@ class FlwtchState extends State<Flwtch> {
shutdownMethodChannel.setMethodCallHandler(modalShutdown);
print("initState: creating cwtchnotifier, ffi");
if (Platform.isAndroid) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList);
cwtch = CwtchGomobile(cwtchNotifier);
} else if (Platform.isLinux) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState);
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList);
cwtch = CwtchFfi(cwtchNotifier);
} else {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList);
cwtch = CwtchFfi(cwtchNotifier);
}
print("initState: invoking cwtch.Start()");
@ -81,6 +83,7 @@ class FlwtchState extends State<Flwtch> {
ChangeNotifierProvider<AppState> getAppStateProvider() => ChangeNotifierProvider.value(value: globalAppState);
Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs);
ChangeNotifierProvider<ServerListState> getServerListStateProvider() => ChangeNotifierProvider.value(value: globalServersList);
@override
Widget build(BuildContext context) {
@ -93,6 +96,7 @@ class FlwtchState extends State<Flwtch> {
getErrorHandlerProvider(),
getTorStatusProvider(),
getAppStateProvider(),
getServerListStateProvider(),
],
builder: (context, widget) {
return Consumer2<Settings, AppState>(
@ -104,7 +108,7 @@ class FlwtchState extends State<Flwtch> {
supportedLocales: AppLocalizations.supportedLocales,
title: 'Cwtch',
theme: mkThemeData(settings),
home: appState.cwtchInit == true ? ShiftRightFixer(child: ProfileMgrView()) : SplashView(),
home: (!appState.cwtchInit || appState.modalState != ModalState.none) ? SplashView() : ProfileMgrView(),
),
);
},
@ -115,13 +119,13 @@ class FlwtchState extends State<Flwtch> {
// the MyBroadcastReceiver method channel
Future<void> modalShutdown(MethodCall mc) async {
// set up the buttons
Widget cancelButton = TextButton(
Widget cancelButton = ElevatedButton(
child: Text(AppLocalizations.of(navKey.currentContext!)!.cancel),
onPressed: () {
Navigator.of(navKey.currentContext!).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
Widget continueButton = ElevatedButton(
child: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchAction),
onPressed: () {
// Directly call the shutdown command, Android will do this for us...
@ -155,7 +159,7 @@ class FlwtchState extends State<Flwtch> {
Future.delayed(Duration(seconds: 2)).then((value) {
if (Platform.isAndroid) {
SystemNavigator.pop();
} else if (Platform.isLinux || Platform.isWindows) {
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
print("Exiting...");
exit(0);
}
@ -168,6 +172,7 @@ class FlwtchState extends State<Flwtch> {
var args = jsonDecode(call.arguments);
var profile = profs.getProfile(args["ProfileOnion"])!;
var convo = profile.contactList.getContact(args["Handle"])!;
Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages;
convo.unreadMessages = 0;
// single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation

View File

@ -5,7 +5,7 @@ import 'package:cwtch/errorHandler.dart';
import 'package:cwtch/settings.dart';
import 'licenses.dart';
import 'main.dart';
import 'opaque.dart';
import 'themes/opaque.dart';
import 'dart:convert';
import 'dart:io';
@ -14,7 +14,7 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:glob/glob.dart';
var globalSettings = Settings(Locale("en", ''), OpaqueDark());
var globalSettings = Settings(Locale("en", ''), CwtchDark());
var globalErrorHandler = ErrorHandler();
void main() {

View File

@ -1,15 +1,10 @@
import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/messagebubble.dart';
import 'package:provider/provider.dart';
import 'dart:async';
import 'dart:collection';
import 'cwtch/cwtch.dart';
import 'main.dart';
import 'package:cwtch/models/profileservers.dart';
////////////////////
/// UI State ///
@ -31,6 +26,79 @@ class ChatMessage {
};
}
enum ModalState {
none,
storageMigration
}
class AppState extends ChangeNotifier {
bool cwtchInit = false;
ModalState modalState = ModalState.none;
bool cwtchIsClosing = false;
String appError = "";
String? _selectedProfile;
int? _selectedConversation;
int _initialScrollIndex = 0;
int _hoveredIndex = -1;
int? _selectedIndex;
bool _unreadMessagesBelow = false;
void SetCwtchInit() {
cwtchInit = true;
notifyListeners();
}
void SetAppError(String error) {
appError = error;
notifyListeners();
}
void SetModalState(ModalState newState) {
modalState = newState;
notifyListeners();
}
String? get selectedProfile => _selectedProfile;
set selectedProfile(String? newVal) {
this._selectedProfile = newVal;
notifyListeners();
}
int? get selectedConversation => _selectedConversation;
set selectedConversation(int? newVal) {
this._selectedConversation = newVal;
notifyListeners();
}
int? get selectedIndex => _selectedIndex;
set selectedIndex(int? newVal) {
this._selectedIndex = newVal;
notifyListeners();
}
// Never use this for message lookup - can be a non-indexed value
// e.g. -1
int get hoveredIndex => _hoveredIndex;
set hoveredIndex(int newVal) {
this._hoveredIndex = newVal;
notifyListeners();
}
bool get unreadMessagesBelow => _unreadMessagesBelow;
set unreadMessagesBelow(bool newVal) {
this._unreadMessagesBelow = newVal;
notifyListeners();
}
int get initialScrollIndex => _initialScrollIndex;
set initialScrollIndex(int newVal) {
this._initialScrollIndex = newVal;
notifyListeners();
}
bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height;
}
///////////////////
/// Providers ///
///////////////////
@ -62,46 +130,8 @@ class ProfileListState extends ChangeNotifier {
}
}
class AppState extends ChangeNotifier {
bool cwtchInit = false;
bool cwtchIsClosing = false;
String appError = "";
String? _selectedProfile;
String? _selectedConversation;
int? _selectedIndex;
void SetCwtchInit() {
cwtchInit = true;
notifyListeners();
}
void SetAppError(String error) {
appError = error;
notifyListeners();
}
String? get selectedProfile => _selectedProfile;
set selectedProfile(String? newVal) {
this._selectedProfile = newVal;
notifyListeners();
}
String? get selectedConversation => _selectedConversation;
set selectedConversation(String? newVal) {
this._selectedConversation = newVal;
notifyListeners();
}
int? get selectedIndex => _selectedIndex;
set selectedIndex(int? newVal) {
this._selectedIndex = newVal;
notifyListeners();
}
bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height;
}
class ContactListState extends ChangeNotifier {
ProfileServerListState? servers;
List<ContactInfoState> _contacts = [];
String _filter = "";
int get num => _contacts.length;
@ -113,6 +143,10 @@ class ContactListState extends ChangeNotifier {
notifyListeners();
}
void connectServers(ProfileServerListState servers) {
this.servers = servers;
}
List<ContactInfoState> filteredList() {
if (!isFiltered) return contacts;
return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList();
@ -120,11 +154,22 @@ class ContactListState extends ChangeNotifier {
void addAll(Iterable<ContactInfoState> newContacts) {
_contacts.addAll(newContacts);
servers?.clearGroups();
_contacts.forEach((contact) {
if (contact.isGroup) {
servers?.addGroup(contact);
}
});
resort();
notifyListeners();
}
void add(ContactInfoState newContact) {
_contacts.add(newContact);
if (newContact.isGroup) {
servers?.addGroup(newContact);
}
resort();
notifyListeners();
}
@ -132,9 +177,18 @@ class ContactListState extends ChangeNotifier {
_contacts.sort((ContactInfoState a, ContactInfoState b) {
// return -1 = a first in list
// return 1 = b first in list
// blocked contacts last
if (a.isBlocked == true && b.isBlocked != true) return 1;
if (a.isBlocked != true && b.isBlocked == true) return -1;
// archive is next...
if (!a.isArchived && b.isArchived) return -1;
if (a.isArchived && !b.isArchived) return 1;
// unapproved top
if (a.isInvitation && !b.isInvitation) return -1;
if (!a.isInvitation && b.isInvitation) return 1;
// special sorting for contacts with no messages in either history
if (a.lastMessageTime.millisecondsSinceEpoch == 0 && b.lastMessageTime.millisecondsSinceEpoch == 0) {
// online contacts first
@ -153,38 +207,53 @@ class ContactListState extends ChangeNotifier {
//} </todo>
}
void updateLastMessageTime(String forOnion, DateTime newVal) {
var contact = getContact(forOnion);
void updateLastMessageTime(int forIdentifier, DateTime newMessageTime) {
var contact = getContact(forIdentifier);
if (contact == null) return;
contact.lastMessageTime = newVal;
resort();
// Assert that the new time is after the current last message time AND that
// new message time is before the current time.
if (newMessageTime.isAfter(contact.lastMessageTime)) {
if (newMessageTime.isBefore(DateTime.now().toLocal())) {
contact.lastMessageTime = newMessageTime;
} else {
// Otherwise set the last message time to now...
contact.lastMessageTime = DateTime.now().toLocal();
}
resort();
}
}
List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
ContactInfoState? getContact(String onion) {
int idx = _contacts.indexWhere((element) => element.onion == onion);
ContactInfoState? getContact(int identifier) {
int idx = _contacts.indexWhere((element) => element.identifier == identifier);
return idx >= 0 ? _contacts[idx] : null;
}
void removeContact(String onion) {
int idx = _contacts.indexWhere((element) => element.onion == onion);
void removeContact(int identifier) {
int idx = _contacts.indexWhere((element) => element.identifier == identifier);
if (idx >= 0) {
_contacts.removeAt(idx);
notifyListeners();
}
}
ContactInfoState? findContact(String byHandle) {
int idx = _contacts.indexWhere((element) => element.onion == byHandle);
return idx >= 0 ? _contacts[idx] : null;
}
}
class ProfileInfoState extends ChangeNotifier {
ProfileServerListState _servers = ProfileServerListState();
ContactListState _contacts = ContactListState();
ServerListState _servers = ServerListState();
final String onion;
String _nickname = "";
String _imagePath = "";
int _unreadMessages = 0;
bool _online = false;
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
// assume profiles are encrypted...this will be set to false
// in the constructor if the profile is encrypted with the defacto password.
@ -206,30 +275,32 @@ class ProfileInfoState extends ChangeNotifier {
this._online = online;
this._encrypted = encrypted;
_contacts.connectServers(this._servers);
if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
this.replaceServers(serversJson);
List<dynamic> contacts = jsonDecode(contactsJson);
this._contacts.addAll(contacts.map((contact) {
return ContactInfoState(this.onion, contact["onion"],
return ContactInfoState(this.onion, contact["identifier"], contact["onion"],
nickname: contact["name"],
status: contact["status"],
imagePath: contact["picture"],
isBlocked: contact["authorization"] == "blocked",
isInvitation: contact["authorization"] == "unknown",
authorization: stringToContactAuthorization(contact["authorization"]),
savePeerHistory: contact["saveConversationHistory"],
numMessages: contact["numMessages"],
numUnread: contact["numUnread"],
isGroup: contact["isGroup"],
server: contact["groupServer"],
archived: contact["isArchived"] == true,
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])));
}));
// dummy set to invoke sort-on-load
if (this._contacts.num > 0) {
this._contacts.updateLastMessageTime(this._contacts._contacts.first.onion, this._contacts._contacts.first.lastMessageTime);
this._contacts.updateLastMessageTime(this._contacts._contacts.first.identifier, this._contacts._contacts.first.lastMessageTime);
}
}
this.replaceServers(serversJson);
}
// Parse out the server list json into our server info state struct...
@ -238,15 +309,22 @@ class ProfileInfoState extends ChangeNotifier {
List<dynamic> servers = jsonDecode(serversJson);
this._servers.replace(servers.map((server) {
// TODO Keys...
return ServerInfoState(onion: server["onion"], status: server["status"]);
return RemoteServerInfoState(onion: server["onion"], identifier: server["identifier"], description: server["description"], status: server["status"]);
}));
this._contacts.contacts.forEach((contact) {
if (contact.isGroup) {
_servers.addGroup(contact);
}
});
notifyListeners();
}
}
//
void updateServerStatusCache(String server, String status) {
this._servers.updateServerCache(server, status);
this._servers.updateServerState(server, status);
notifyListeners();
}
@ -287,7 +365,7 @@ class ProfileInfoState extends ChangeNotifier {
}
ContactListState get contactList => this._contacts;
ServerListState get serverList => this._servers;
ProfileServerListState get serverList => this._servers;
@override
void dispose() {
@ -312,12 +390,12 @@ class ProfileInfoState extends ChangeNotifier {
} else {
this._contacts.add(ContactInfoState(
this.onion,
contact["identifier"],
contact["onion"],
nickname: contact["name"],
status: contact["status"],
imagePath: contact["picture"],
isBlocked: contact["authorization"] == "blocked",
isInvitation: contact["authorization"] == "unknown",
authorization: stringToContactAuthorization(contact["authorization"]),
savePeerHistory: contact["saveConversationHistory"],
numMessages: contact["numMessages"],
numUnread: contact["numUnread"],
@ -329,15 +407,159 @@ class ProfileInfoState extends ChangeNotifier {
});
}
}
void downloadInit(String fileKey, int numChunks) {
this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
}
void downloadUpdate(String fileKey, int progress, int numChunks) {
if (!downloadActive(fileKey)) {
this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
if (progress < 0) {
this._downloads[fileKey]!.interrupted = true;
}
} else {
if (this._downloads[fileKey]!.interrupted) {
this._downloads[fileKey]!.interrupted = false;
}
this._downloads[fileKey]!.chunksDownloaded = progress;
this._downloads[fileKey]!.chunksTotal = numChunks;
}
notifyListeners();
}
void downloadMarkManifest(String fileKey) {
if (!downloadActive(fileKey)) {
this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now());
}
this._downloads[fileKey]!.gotManifest = true;
notifyListeners();
}
void downloadMarkFinished(String fileKey, String finalPath) {
if (!downloadActive(fileKey)) {
// happens as a result of a CheckDownloadStatus call,
// invoked from a historical (timeline) download message
// so setting numChunks correctly shouldn't matter
this.downloadInit(fileKey, 1);
}
// only update if different
if (!this._downloads[fileKey]!.complete) {
this._downloads[fileKey]!.timeEnd = DateTime.now();
this._downloads[fileKey]!.downloadedTo = finalPath;
this._downloads[fileKey]!.complete = true;
notifyListeners();
}
}
bool downloadKnown(String fileKey) {
return this._downloads.containsKey(fileKey);
}
bool downloadActive(String fileKey) {
return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted;
}
bool downloadGotManifest(String fileKey) {
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.gotManifest;
}
bool downloadComplete(String fileKey) {
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete;
}
bool downloadInterrupted(String fileKey) {
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted;
}
void downloadMarkResumed(String fileKey) {
if (this._downloads.containsKey(fileKey)) {
this._downloads[fileKey]!.interrupted = false;
}
}
double downloadProgress(String fileKey) {
return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0;
}
// used for loading interrupted download info; use downloadMarkFinished for successful downloads
void downloadSetPath(String fileKey, String path) {
if (this._downloads.containsKey(fileKey)) {
this._downloads[fileKey]!.downloadedTo = path;
}
}
String? downloadFinalPath(String fileKey) {
return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null;
}
String downloadSpeed(String fileKey) {
if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) {
return "0 B/s";
}
var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096;
var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds;
if (seconds == 0) {
return "0 B/s";
}
return prettyBytes((bytes / seconds).round()) + "/s";
}
}
class FileDownloadProgress {
int chunksDownloaded = 0;
int chunksTotal = 1;
bool complete = false;
bool gotManifest = false;
bool interrupted = false;
String? downloadedTo;
DateTime? timeStart;
DateTime? timeEnd;
FileDownloadProgress(this.chunksTotal, this.timeStart);
double progress() {
return 1.0 * chunksDownloaded / chunksTotal;
}
}
String prettyBytes(int bytes) {
if (bytes > 1000000000) {
return (1.0 * bytes / 1000000000).toStringAsFixed(1) + " GB";
} else if (bytes > 1000000) {
return (1.0 * bytes / 1000000).toStringAsFixed(1) + " MB";
} else if (bytes > 1000) {
return (1.0 * bytes / 1000).toStringAsFixed(1) + " kB";
} else {
return bytes.toString() + " B";
}
}
enum ContactAuthorization { unknown, approved, blocked }
ContactAuthorization stringToContactAuthorization(String authStr) {
switch (authStr) {
case "approved":
return ContactAuthorization.approved;
case "blocked":
return ContactAuthorization.blocked;
default:
return ContactAuthorization.unknown;
}
}
class MessageCache {
final MessageMetadata metadata;
final String wrapper;
MessageCache(this.metadata, this.wrapper);
}
class ContactInfoState extends ChangeNotifier {
final String profileOnion;
final int identifier;
final String onion;
late String _nickname;
late bool _isInvitation;
late bool _isBlocked;
late ContactAuthorization _authorization;
late String _status;
late String _imagePath;
late String _savePeerHistory;
@ -345,30 +567,30 @@ class ContactInfoState extends ChangeNotifier {
late int _totalMessages = 0;
late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageRowState>> keys;
late List<MessageCache?> messageCache;
int _newMarker = 0;
DateTime _newMarkerClearAt = DateTime.now();
// todo: a nicer way to model contacts, groups and other "entities"
late bool _isGroup;
String? _server;
late bool _archived;
ContactInfoState(
this.profileOnion,
this.onion, {
nickname = "",
isGroup = false,
isInvitation = false,
isBlocked = false,
status = "",
imagePath = "",
savePeerHistory = "DeleteHistoryConfirmed",
numMessages = 0,
numUnread = 0,
lastMessageTime,
server,
}) {
ContactInfoState(this.profileOnion, this.identifier, this.onion,
{nickname = "",
isGroup = false,
authorization = ContactAuthorization.unknown,
status = "",
imagePath = "",
savePeerHistory = "DeleteHistoryConfirmed",
numMessages = 0,
numUnread = 0,
lastMessageTime,
server,
archived = false}) {
this._nickname = nickname;
this._isGroup = isGroup;
this._isInvitation = isInvitation;
this._isBlocked = isBlocked;
this._authorization = authorization;
this._status = status;
this._imagePath = imagePath;
this._totalMessages = numMessages;
@ -376,12 +598,25 @@ class ContactInfoState extends ChangeNotifier {
this._savePeerHistory = savePeerHistory;
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
this._server = server;
this._archived = archived;
this.messageCache = List.empty(growable: true);
keys = Map<String, GlobalKey<MessageRowState>>();
}
String get nickname => this._nickname;
String get savePeerHistory => this._savePeerHistory;
// Indicated whether the conversation is archived, in which case it will
// be moved to the very bottom of the active conversations list until
// new messages appear
set isArchived(bool archived) {
this._archived = archived;
notifyListeners();
}
bool get isArchived => this._archived;
set savePeerHistory(String newVal) {
this._savePeerHistory = newVal;
notifyListeners();
@ -398,15 +633,13 @@ class ContactInfoState extends ChangeNotifier {
notifyListeners();
}
bool get isBlocked => this._isBlocked;
set isBlocked(bool newVal) {
this._isBlocked = newVal;
notifyListeners();
}
bool get isBlocked => this._authorization == ContactAuthorization.blocked;
bool get isInvitation => this._isInvitation;
set isInvitation(bool newVal) {
this._isInvitation = newVal;
bool get isInvitation => this._authorization == ContactAuthorization.unknown;
ContactAuthorization get authorization => this._authorization;
set authorization(ContactAuthorization newAuth) {
this._authorization = newAuth;
notifyListeners();
}
@ -418,10 +651,37 @@ class ContactInfoState extends ChangeNotifier {
int get unreadMessages => this._unreadMessages;
set unreadMessages(int newVal) {
// don't reset newMarker position when unreadMessages is being cleared
if (newVal > 0) {
this._newMarker = newVal;
} else {
this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2));
}
this._unreadMessages = newVal;
notifyListeners();
}
int get newMarker {
if (DateTime.now().isAfter(this._newMarkerClearAt)) {
// perform heresy
this._newMarker = 0;
// no need to notifyListeners() because presumably this getter is
// being called from a renderer anyway
}
return this._newMarker;
}
// what's a getter that sometimes sets without a setter
// that sometimes doesn't set
set newMarker(int newVal) {
// only unreadMessages++ can set newMarker = 1;
// avoids drawing a marker when the convo is already open
if (newVal >= 1) {
this._newMarker = newVal;
notifyListeners();
}
}
int get totalMessages => this._totalMessages;
set totalMessages(int newVal) {
this._totalMessages = newVal;
@ -452,11 +712,37 @@ class ContactInfoState extends ChangeNotifier {
}
}
GlobalKey<MessageRowState> getMessageKey(String index) {
GlobalKey<MessageRowState> getMessageKey(int conversation, int message) {
String index = "c: " + conversation.toString() + " m:" + message.toString();
if (keys[index] == null) {
keys[index] = GlobalKey<MessageRowState>();
}
GlobalKey<MessageRowState> ret = keys[index]!;
return ret;
}
GlobalKey<MessageRowState>? getMessageKeyOrFail(int conversation, int message) {
String index = "c: " + conversation.toString() + " m:" + message.toString();
if (keys[index] == null) {
return null;
}
GlobalKey<MessageRowState> ret = keys[index]!;
return ret;
}
void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) {
this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data));
this.totalMessages += 1;
}
void bumpMessageCache() {
this.messageCache.insert(0, null);
this.totalMessages += 1;
}
void ackCache(int messageID) {
this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true;
notifyListeners();
}
}

View File

@ -1,8 +1,11 @@
import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../model.dart';
import 'messages/filemessage.dart';
import 'messages/invitemessage.dart';
import 'messages/malformedmessage.dart';
import 'messages/quotedmessage.dart';
@ -13,6 +16,7 @@ const TextMessageOverlay = 1;
const QuotedMessageOverlay = 10;
const SuggestContactOverlay = 100;
const InviteGroupOverlay = 101;
const FileShareOverlay = 200;
// Defines the length of the tor v3 onion address. Code using this constant will
// need to updated when we allow multiple different identifiers. At which time
@ -24,91 +28,115 @@ const GroupConversationHandleLength = 32;
abstract class Message {
MessageMetadata getMetadata();
Widget getWidget(BuildContext context);
Widget getWidget(BuildContext context, Key key);
Widget getPreviewWidget(BuildContext context);
}
Future<Message> messageHandler(BuildContext context, String profileOnion, String contactHandle, int index) {
Message compileOverlay(MessageMetadata metadata, String messageData) {
try {
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index);
dynamic message = jsonDecode(messageData);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
case FileShareOverlay:
return FileMessage(metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
}
} catch (e) {
return MalformedMessage(metadata);
}
}
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) {
try {
var cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
if (cache != null && cache.length > index) {
if (cache[index] != null) {
return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper));
}
}
} catch (e) {
// provider check failed...make an expensive call...
}
try {
Future<dynamic> rawMessageEnvelopeFuture;
if (byID) {
rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessageByID(profileOnion, conversationIdentifier, index);
} else {
rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index);
}
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
// There are 2 conditions in which this error condition can be met:
// 1. The application == nil, in which case this instance of the UI is already
// broken beyond repair, and will either be replaced by a new version, or requires a complete
// restart.
// 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion.
// This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the
// calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one
// will find itself delayed.
// The second case is recoverable by tail-recursing this future.
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
return Future.delayed(Duration(seconds: 2), () {
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
return messageHandler(context, profileOnion, contactHandle, index).then((value) => value);
});
}
// Construct the initial metadata
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
var senderHandle = messageWrapper['PeerID'];
var senderImage = messageWrapper['ContactImage'];
var flags = int.parse(messageWrapper['Flags'].toString(), radix: 2);
var ackd = messageWrapper['Acknowledged'];
var error = messageWrapper['Error'] != null;
String? signature;
// If this is a group, store the signature
if (contactHandle.length == GroupConversationHandleLength) {
signature = messageWrapper['Signature'];
}
var metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error);
var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", <String, String>{}, false, true, false);
try {
dynamic message = jsonDecode(messageWrapper['Message']);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
// There are 2 conditions in which this error condition can be met:
// 1. The application == nil, in which case this instance of the UI is already
// broken beyond repair, and will either be replaced by a new version, or requires a complete
// restart.
// 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion.
// This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the
// calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one
// will find itself delayed.
// The second case is recoverable by tail-recursing this future.
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
return Future.delayed(Duration(seconds: 2), () {
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
return messageHandler(context, profileOnion, conversationIdentifier, -1, byID: byID).then((value) => value);
});
}
// Construct the initial metadata
var messageID = messageWrapper['ID'];
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
var senderHandle = messageWrapper['PeerID'];
var senderImage = messageWrapper['ContactImage'];
var attributes = messageWrapper['Attributes'];
var ackd = messageWrapper['Acknowledged'];
var error = messageWrapper['Error'] != null;
var signature = messageWrapper['Signature'];
metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false);
return compileOverlay(metadata, messageWrapper['Message']);
} catch (e) {
EnvironmentConfig.debugLog("an error! " + e.toString());
return MalformedMessage(metadata);
}
});
} catch (e) {
return Future.value(MalformedMessage(MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true)));
return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", <String, String>{}, false, true, false)));
}
}
class MessageMetadata extends ChangeNotifier {
// meta-metadata
final String profileOnion;
final String contactHandle;
final int messageIndex;
final int conversationIdentifier;
final int messageID;
final DateTime timestamp;
final String senderHandle;
final String? senderImage;
int _flags;
final dynamic _attributes;
bool _ackd;
bool _error;
final bool isAuto;
final String? signature;
int get flags => this._flags;
set flags(int newVal) {
this._flags = newVal;
notifyListeners();
}
dynamic get attributes => this._attributes;
bool get ackd => this._ackd;
set ackd(bool newVal) {
@ -122,5 +150,6 @@ class MessageMetadata extends ChangeNotifier {
notifyListeners();
}
MessageMetadata(this.profileOnion, this.contactHandle, this.messageIndex, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error);
MessageMetadata(
this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error, this.isAuto);
}

View File

@ -0,0 +1,86 @@
import 'dart:convert';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/filebubble.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../../main.dart';
import '../../model.dart';
class FileMessage extends Message {
final MessageMetadata metadata;
final String content;
final RegExp nonHex = RegExp(r'[^a-f0-9]');
FileMessage(this.metadata, this.content);
@override
Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
dynamic shareObj = jsonDecode(this.content);
if (shareObj == null) {
return MessageRow(MalformedBubble());
}
String nameSuggestion = shareObj['f'] as String;
String rootHash = shareObj['h'] as String;
String nonce = shareObj['n'] as String;
int fileSize = shareObj['s'] as int;
String fileKey = rootHash + "." + nonce;
if (metadata.attributes["file-downloaded"] == "true") {
if (!Provider.of<ProfileInfoState>(context).downloadKnown(fileKey)) {
Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, fileKey);
}
}
if (!validHash(rootHash, nonce)) {
return MessageRow(MalformedBubble());
}
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize, isAuto: metadata.isAuto), key: key);
});
}
@override
Widget getPreviewWidget(BuildContext context) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
dynamic shareObj = jsonDecode(this.content);
if (shareObj == null) {
return MessageRow(MalformedBubble());
}
String nameSuggestion = shareObj['n'] as String;
String rootHash = shareObj['h'] as String;
String nonce = shareObj['n'] as String;
int fileSize = shareObj['s'] as int;
if (!validHash(rootHash, nonce)) {
return MessageRow(MalformedBubble());
}
return Container(
alignment: Alignment.center,
child: FileBubble(
nameSuggestion,
rootHash,
nonce,
fileSize,
isAuto: metadata.isAuto,
interactive: false,
));
});
}
@override
MessageMetadata getMetadata() {
return this.metadata;
}
bool validHash(String hash, String nonce) {
return hash.length == 128 && nonce.length == 48 && !hash.contains(nonHex) && !nonce.contains(nonHex);
}
}

View File

@ -17,18 +17,17 @@ class InviteMessage extends Message {
InviteMessage(this.overlay, this.metadata, this.content);
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
String inviteTarget;
String inviteNick;
String invite = this.content;
if (this.content.length == TorV3ContactHandleLength) {
inviteTarget = this.content;
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(inviteTarget);
var targetContact = Provider.of<ProfileInfoState>(context).contactList.findContact(inviteTarget);
inviteNick = targetContact == null ? this.content : targetContact.nickname;
} else {
var parts = this.content.toString().split("||");
@ -40,7 +39,7 @@ class InviteMessage extends Message {
return MessageRow(MalformedBubble());
}
}
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: key);
});
}
@ -51,10 +50,10 @@ class InviteMessage extends Message {
builder: (bcontext, child) {
String inviteTarget;
String inviteNick;
String invite = this.content;
if (this.content.length == TorV3ContactHandleLength) {
inviteTarget = this.content;
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(inviteTarget);
var targetContact = Provider.of<ProfileInfoState>(context).contactList.findContact(inviteTarget);
inviteNick = targetContact == null ? this.content : targetContact.nickname;
} else {
var parts = this.content.toString().split("||");
@ -66,7 +65,7 @@ class InviteMessage extends Message {
return MalformedBubble();
}
}
return InvitationBubble(overlay, inviteTarget, inviteNick);
return InvitationBubble(overlay, inviteTarget, inviteNick, invite);
});
}

View File

@ -9,11 +9,11 @@ class MalformedMessage extends Message {
MalformedMessage(this.metadata);
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (context, child) {
return MessageRow(MalformedBubble());
return MessageRow(MalformedBubble(), key: key);
});
}

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:cwtch/widgets/quotedmessage.dart';
import 'package:flutter/widgets.dart';
@ -10,6 +11,17 @@ import 'package:provider/provider.dart';
import '../../main.dart';
import '../../model.dart';
class QuotedMessageStructure {
final String quotedHash;
final String body;
QuotedMessageStructure(this.quotedHash, this.body);
Map<String, dynamic> toJson() => {
'quotedHash': quotedHash,
'body': body,
};
}
class LocallyIndexedMessage {
final dynamic message;
final int index;
@ -40,7 +52,7 @@ class QuotedMessage extends Message {
dynamic message = jsonDecode(this.content);
return Text(message["body"]);
} catch (e) {
return MalformedMessage(this.metadata).getWidget(context);
return MalformedBubble();
}
});
}
@ -51,16 +63,15 @@ class QuotedMessage extends Message {
}
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
try {
dynamic message = jsonDecode(this.content);
if (message["body"] == null || message["quotedHash"] == null) {
return MalformedMessage(this.metadata).getWidget(context);
return MalformedBubble();
}
var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]);
int messageIndex = metadata.messageIndex;
var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]);
Future<LocallyIndexedMessage?> quotedMessage = quotedMessagePotentials.then((matchingMessages) {
if (matchingMessages == "[]") {
return null;
@ -70,9 +81,7 @@ class QuotedMessage extends Message {
// message
try {
var list = (jsonDecode(matchingMessages) as List<dynamic>).map((data) => LocallyIndexedMessage.fromJson(data)).toList();
LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () {
return list.firstWhere((element) => messageIndex > element.index);
});
LocallyIndexedMessage candidate = list.reversed.first;
return candidate;
} catch (e) {
// Malformed Message will be returned...
@ -83,18 +92,17 @@ class QuotedMessage extends Message {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
return MessageRow(
QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
if (localIndex != null) {
return messageHandler(context, metadata.profileOnion, metadata.contactHandle, localIndex.index);
return messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index);
}
return MalformedMessage(this.metadata);
})),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
key: key);
});
} catch (e) {
return MalformedMessage(this.metadata).getWidget(context);
return MalformedBubble();
}
}
}

View File

@ -1,6 +1,10 @@
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagebubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
@ -27,12 +31,14 @@ class TextMessage extends Message {
}
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
return MessageRow(MessageBubble(this.content), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
return MessageRow(
MessageBubble(this.content),
key: key,
);
});
}
}

View File

@ -0,0 +1,93 @@
import 'package:cwtch/model.dart';
import 'package:flutter/material.dart';
class ProfileServerListState extends ChangeNotifier {
List<RemoteServerInfoState> _servers = [];
void replace(Iterable<RemoteServerInfoState> newServers) {
_servers.clear();
_servers.addAll(newServers);
resort();
notifyListeners();
}
RemoteServerInfoState? getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null;
}
void updateServerState(String onion, String status) {
int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_servers[idx].status = status;
} else {
print("Tried to update server cache without a starting state...this is probably an error");
}
resort();
notifyListeners();
}
void resort() {
_servers.sort((RemoteServerInfoState a, RemoteServerInfoState b) {
// return -1 = a first in list
// return 1 = b first in list
// online v offline
if (a.status == "Synced" && b.status != "Synced") {
return -1;
} else if (a.status != "Synced" && b.status == "Synced") {
return 1;
}
// num of groups
if (a.groups.length > b.groups.length) {
return -1;
} else if (b.groups.length > a.groups.length) {
return 1;
}
return 0;
});
}
void clearGroups() {
_servers.map((server) => server.clearGroups());
}
void addGroup(ContactInfoState group) {
int idx = _servers.indexWhere((element) => element.onion == group.server);
if (idx >= 0) {
_servers[idx].addGroup(group);
}
}
List<RemoteServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
}
class RemoteServerInfoState extends ChangeNotifier {
final String onion;
final int identifier;
String status;
String description;
List<ContactInfoState> _groups = [];
RemoteServerInfoState({required this.onion, required this.identifier, required this.description, required this.status});
void updateDescription(String newDescription) {
this.description = newDescription;
notifyListeners();
}
void clearGroups() {
_groups = [];
}
void addGroup(ContactInfoState group) {
_groups.add(group);
notifyListeners();
}
List<ContactInfoState> get groups => _groups.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
}

View File

@ -9,28 +9,98 @@ class ServerListState extends ChangeNotifier {
notifyListeners();
}
void clear() {
_servers.clear();
}
ServerInfoState? getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null;
}
void updateServerCache(String onion, String status) {
void add(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) {
var sis = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted);
int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_servers[idx] = ServerInfoState(onion: onion, status: status);
_servers[idx] = sis;
} else {
print("Tried to update server cache without a starting state...this is probably an error");
_servers.add(ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted));
}
notifyListeners();
}
void updateServer(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) {
int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_servers[idx] = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted);
} else {
print("Tried to update server list without a starting state...this is probably an error");
}
notifyListeners();
}
void updateServerStats(String onion, int newTotalMessages, int newConnections) {
var server = getServer(onion);
if (server != null) {
server.setStats(newTotalMessages, newConnections);
resort();
notifyListeners();
}
}
void delete(String onion) {
_servers.removeWhere((element) => element.onion == onion);
notifyListeners();
}
void resort() {
_servers.sort((ServerInfoState a, ServerInfoState b) {
// return -1 = a first in list
// return 1 = b first in list
if (a.totalMessages > b.totalMessages) {
return -1;
} else if (b.totalMessages > a.totalMessages) {
return 1;
}
return 0;
});
}
List<ServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
}
class ServerInfoState extends ChangeNotifier {
final String onion;
final String status;
String onion;
String serverBundle;
String description;
bool running;
bool autoStart;
bool isEncrypted;
int totalMessages = 0;
int connections = 0;
ServerInfoState({required this.onion, required this.status});
ServerInfoState({required this.onion, required this.serverBundle, required this.running, required this.description, required this.autoStart, required this.isEncrypted});
void setAutostart(bool val) {
autoStart = val;
notifyListeners();
}
void setRunning(bool val) {
running = val;
notifyListeners();
}
void setDescription(String val) {
description = val;
notifyListeners();
}
void setStats(int newTotalMessages, int newConnections) {
totalMessages = newTotalMessages;
connections = newConnections;
notifyListeners();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,18 @@ import 'dart:collection';
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'opaque.dart';
import 'themes/opaque.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
const TapirGroupsExperiment = "tapir-groups-experiment";
const ServerManagementExperiment = "servers-experiment";
const FileSharingExperiment = "filesharing";
const ImagePreviewsExperiment = "filesharing-images";
const ClickableLinksExperiment = "clickable-links";
enum DualpaneMode {
Single,
@ -31,16 +36,11 @@ class Settings extends ChangeNotifier {
DualpaneMode _uiColumnModeLandscape = DualpaneMode.CopyPortrait;
bool blockUnknownConnections = false;
bool streamerMode = false;
String _downloadPath = "";
/// Set the dark theme.
void setDark() {
theme = OpaqueDark();
notifyListeners();
}
/// Set the Light theme.
void setLight() {
theme = OpaqueLight();
void setTheme(String themeId, String mode) {
theme = getTheme(themeId, mode);
notifyListeners();
}
@ -65,20 +65,16 @@ class Settings extends ChangeNotifier {
/// be sent to the function and new settings will be instantiated based on the contents.
handleUpdate(dynamic settings) {
// Set Theme and notify listeners
if (settings["Theme"] == "light") {
this.setLight();
} else {
this.setDark();
}
this.setTheme(settings["Theme"], settings["ThemeMode"] ?? mode_dark);
// Set Locale and notify listeners
switchLocale(Locale(settings["Locale"]));
// Decide whether to enable Experiments
blockUnknownConnections = settings["BlockUnknownConnections"];
blockUnknownConnections = settings["BlockUnknownConnections"] ?? false;
streamerMode = settings["StreamerMode"] ?? false;
// Decide whether to enable Experiments
experimentsEnabled = settings["ExperimentsEnabled"];
experimentsEnabled = settings["ExperimentsEnabled"] ?? false;
// Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON
experiments = new HashMap<String, bool>.from(settings["Experiments"]);
@ -87,6 +83,9 @@ class Settings extends ChangeNotifier {
_uiColumnModePortrait = uiColumnModeFromString(settings["UIColumnModePortrait"]);
_uiColumnModeLandscape = uiColumnModeFromString(settings["UIColumnModeLandscape"]);
// auto-download folder
_downloadPath = settings["DownloadPath"] ?? "";
// Push the experimental settings to Consumers of Settings
notifyListeners();
}
@ -105,6 +104,11 @@ class Settings extends ChangeNotifier {
notifyListeners();
}
setStreamerMode(bool newSteamerMode) {
streamerMode = newSteamerMode;
notifyListeners();
}
/// Block Unknown Connections will autoblock connections if they authenticate with public key not in our contacts list.
/// This is one of the best tools we have to combat abuse, while it isn't ideal it does allow a user to curate their contacts
/// list without being bothered by spurious requests (either permanently, or as a short term measure).
@ -214,25 +218,46 @@ class Settings extends ChangeNotifier {
}
}
// checks experiment settings and file extension for image previews
// (ignores file size; if the user manually accepts the file, assume it's okay to preview)
bool shouldPreview(String path) {
var lpath = path.toLowerCase();
return isExperimentEnabled(ImagePreviewsExperiment) && (
lpath.endsWith(".jpg") ||
lpath.endsWith(".jpeg") ||
lpath.endsWith(".png") ||
lpath.endsWith(".gif") ||
lpath.endsWith(".webp") ||
lpath.endsWith(".bmp")
);
}
String get downloadPath => _downloadPath;
set downloadPath(String newval) {
_downloadPath = newval;
notifyListeners();
}
/// Construct a default settings object.
Settings(this.locale, this.theme);
/// Convert this Settings object to a JSON representation for serialization on the
/// event bus.
dynamic asJson() {
var themeString = theme.identifier();
return {
"Locale": this.locale.languageCode,
"Theme": themeString,
"Theme": theme.theme,
"ThemeMode": theme.mode,
"PreviousPid": -1,
"BlockUnknownConnections": blockUnknownConnections,
"StreamerMode": streamerMode,
"ExperimentsEnabled": this.experimentsEnabled,
"Experiments": experiments,
"StateRootPane": 0,
"FirstTime": false,
"UIColumnModePortrait": uiColumnModePortrait.toString(),
"UIColumnModeLandscape": uiColumnModeLandscape.toString(),
"DownloadPath": _downloadPath,
};
}
}

123
lib/themes/cwtch.dart Normal file
View File

@ -0,0 +1,123 @@
import 'dart:ui';
import 'dart:core';
import 'package:flutter/material.dart';
import 'opaque.dart';
const cwtch_theme = "cwtch";
final Color darkGreyPurple = Color(0xFF281831);
final Color deepPurple = Color(0xFF422850);
final Color mauvePurple = Color(0xFF8E64A5);
final Color whiteishPurple = Color(0xFFE3DFE4);
final Color lightGrey = Color(0xFF9E9E9E);
final Color softGreen = Color(0xFFA0FFB0);
final Color softRed = Color(0xFFFFA0B0);
final Color whitePurple = Color(0xFFFFFDFF);
final Color softPurple = Color(0xFFFDF3FC);
final Color purple = Color(0xFFDFB9DE);
final Color brightPurple = Color(0xFFD1B0E0); // not in new: portrait badge color
final Color darkPurple = Color(0xFF350052);
final Color greyPurple = Color(0xFF775F84); // not in new: portrait borders
final Color pink = Color(0xFFE85DA1); // not in new: active button color
final Color hotPink = Color(0xFFD20070); // Color(0xFFD01972);
final Color softGrey = Color(0xFFB3B6B3); // not in new theme: blocked
OpaqueThemeType GetCwtchTheme(String mode) {
if (mode == mode_dark) {
return CwtchDark();
} else {
return CwtchLight();
}
}
class CwtchDark extends OpaqueThemeType {
static final Color background = darkGreyPurple;
static final Color header = darkGreyPurple;
static final Color userBubble = mauvePurple;
static final Color peerBubble = deepPurple;
static final Color font = whiteishPurple;
static final Color settings = whiteishPurple;
static final Color accent = hotPink;
get theme => cwtch_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get backgroundHilightElementColor => deepPurple;
get mainTextColor => font; //whiteishPurple;
get sendHintTextColor => mauvePurple;
get hilightElementColor => purple;
get defaultButtonColor => accent; //hotPink;
get defaultButtonTextColor => whiteishPurple;
get defaultButtonDisabledColor => lightGrey;
get defaultButtonDisabledTextColor => darkGreyPurple;
get textfieldBackgroundColor => deepPurple;
get textfieldBorderColor => deepPurple;
get textfieldHintColor => mainTextColor; //TODO pick
get textfieldErrorColor => hotPink;
get scrollbarDefaultColor => purple;
get portraitBackgroundColor => deepPurple;
get portraitOnlineBorderColor => whiteishPurple;
get portraitOfflineBorderColor => purple;
get portraitBlockedBorderColor => lightGrey;
get portraitBlockedTextColor => lightGrey;
get portraitContactBadgeColor => hotPink;
get portraitContactBadgeTextColor => whiteishPurple;
get portraitProfileBadgeColor => mauvePurple;
get portraitProfileBadgeTextColor => darkGreyPurple;
get dropShadowColor => mauvePurple;
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class CwtchLight extends OpaqueThemeType {
static final Color background = whitePurple;
static final Color header = softPurple;
static final Color userBubble = purple;
static final Color peerBubble = softPurple;
static final Color font = darkPurple;
static final Color settings = darkPurple;
static final Color accent = hotPink;
get theme => cwtch_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple;
get backgroundHilightElementColor => softPurple;
get mainTextColor => settings;
get sendHintTextColor => purple;
get hilightElementColor => purple; //darkPurple; // todo shouldn't be this, too dark, makes font unreadable
get defaultButtonColor => accent; // hotPink;
get defaultButtonTextColor => whitePurple; // ?
get defaultButtonDisabledColor => softGrey;
get textfieldBackgroundColor => purple;
get textfieldBorderColor => purple;
get textfieldHintColor => font; //TODO pick
get textfieldErrorColor => hotPink;
get scrollbarDefaultColor => accent;
get portraitBackgroundColor => softPurple;
get portraitOnlineBorderColor => greyPurple;
get portraitOfflineBorderColor => greyPurple;
get portraitBlockedBorderColor => softGrey;
get portraitBlockedTextColor => softGrey;
get portraitContactBadgeColor => accent;
get portraitContactBadgeTextColor => whitePurple;
get portraitProfileBadgeColor => brightPurple;
get portraitProfileBadgeTextColor => whitePurple;
get dropShadowColor => purple;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

69
lib/themes/ghost.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'opaque.dart';
const ghost_theme = "ghost";
OpaqueThemeType GetGhostTheme(String mode) {
if (mode == mode_dark) {
return GhostDark();
} else {
return GhostLight();
}
}
class GhostDark extends CwtchDark {
static final Color background = Color(0xFF0D0D1F);
static final Color header = Color(0xFF0D0D1F);
static final Color userBubble = Color(0xFF1A237E);
static final Color peerBubble = Color(0xFF000051);
static final Color font = Color(0xFFFFFFFF);
static final Color settings = Color(0xFFFDFFFD);
static final Color accent = Color(0xFFD20070);
get theme => ghost_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get mainTextColor => font; //whiteishPurple;
get defaultButtonColor => accent; //hotPink;
get textfieldHintColor => mainTextColor; //TODO pick
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class GhostLight extends CwtchLight {
static final Color background = Color(0xFFFDFDFF);
static final Color header = Color(0xFFAAB6FE);
static final Color userBubble = Color(0xFFAAB6FE);
static final Color peerBubble = Color(0xFFE8EAF6);
static final Color font = Color(0xFF0D0D1F);
static final Color settings = Color(0xFF0D0D1F);
static final Color accent = Color(0xFFD20070);
get theme => ghost_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple;
get mainTextColor => settings;
get defaultButtonColor => accent; // hotPink;
get textfieldHintColor => font; //TODO pick
get scrollbarDefaultColor => accent;
get portraitContactBadgeColor => accent;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

69
lib/themes/mermaid.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'opaque.dart';
const mermaid_theme = "mermaid";
OpaqueThemeType GetMermaidTheme(String mode) {
if (mode == mode_dark) {
return MermaidDark();
} else {
return MermaidLight();
}
}
class MermaidDark extends CwtchDark {
static final Color background = Color(0xFF102426);
static final Color header = Color(0xFF102426);
static final Color userBubble = Color(0xFF00838F);
static final Color peerBubble = Color(0xFF00363A);
static final Color font = Color(0xFFFFFFFF);
static final Color settings = Color(0xFFF7FCFD);
static final Color accent = Color(0xFF8E64A5);
get theme => mermaid_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get mainTextColor => font; //whiteishPurple;
get defaultButtonColor => accent; //hotPink;
get textfieldHintColor => mainTextColor; //TODO pick
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class MermaidLight extends CwtchLight {
static final Color background = Color(0xFFF7FCFD);
static final Color header = Color(0xFF56C8D8);
static final Color userBubble = Color(0xFF56C8D8);
static final Color peerBubble = Color(0xFFB2EBF2);
static final Color font = Color(0xFF102426);
static final Color settings = Color(0xFF102426);
static final Color accent = Color(0xFF8E64A5);
get theme => mermaid_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple;
get mainTextColor => settings;
get defaultButtonColor => accent; // hotPink;
get textfieldHintColor => font; //TODO pick
get scrollbarDefaultColor => accent;
get portraitContactBadgeColor => accent;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

69
lib/themes/midnight.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'opaque.dart';
const midnight_theme = "midnight";
OpaqueThemeType GetMidnightTheme(String mode) {
if (mode == mode_dark) {
return MidnightDark();
} else {
return MidnightLight();
}
}
class MidnightDark extends CwtchDark {
static final Color background = Color(0xFF1B1B1B);
static final Color header = Color(0xFF1B1B1B);
static final Color userBubble = Color(0xFF373737);
static final Color peerBubble = Color(0xFF494949);
static final Color font = Color(0xFFFFFFFF);
static final Color settings = Color(0xFFFFFDFF);
static final Color accent = Color(0xFFD20070);
get theme => midnight_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get mainTextColor => font; //whiteishPurple;
get defaultButtonColor => accent; //hotPink;
get textfieldHintColor => mainTextColor; //TODO pick
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class MidnightLight extends CwtchLight {
static final Color background = Color(0xFFFFFDFF);
static final Color header = Color(0xFFE0E0E0);
static final Color userBubble = Color(0xFFE0E0E0);
static final Color peerBubble = Color(0xFFBABDBE);
static final Color font = Color(0xFF1B1B1B);
static final Color settings = Color(0xFF1B1B1B);
static final Color accent = Color(0xFFD20070);
get theme => midnight_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple;
get mainTextColor => settings;
get defaultButtonColor => accent; // hotPink;
get textfieldHintColor => font; //TODO pick
get scrollbarDefaultColor => accent;
get portraitContactBadgeColor => accent;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

69
lib/themes/neon1.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'opaque.dart';
const neon1_theme = "neon1";
OpaqueThemeType GetNeon1Theme(String mode) {
if (mode == mode_dark) {
return Neon1Dark();
} else {
return Neon1Light();
}
}
class Neon1Dark extends CwtchDark {
static final Color background = Color(0xFF290826);
static final Color header = Color(0xFF290826);
static final Color userBubble = Color(0xFFD20070);
static final Color peerBubble = Color(0xFF26A9A4);
static final Color font = Color(0xFFFFFFFF);
static final Color settings = Color(0xFFFFFDFF);
static final Color accent = Color(0xFFA604FE);
get theme => neon1_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get mainTextColor => font; //whiteishPurple;
get defaultButtonColor => accent; //hotPink;
get textfieldHintColor => mainTextColor; //TODO pick
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class Neon1Light extends CwtchLight {
static final Color background = Color(0xFFFFFDFF);
static final Color header = Color(0xFFFF94C2);
static final Color userBubble = Color(0xFFFF94C2);
static final Color peerBubble = Color(0xFFE7F6F6);
static final Color font = Color(0xFF290826);
static final Color settings = Color(0xFF290826);
static final Color accent = Color(0xFFA604FE);
get theme => neon1_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple
get mainTextColor => settings;
get defaultButtonColor => accent; // hotPink;
get textfieldHintColor => font; //TODO pick
get scrollbarDefaultColor => accent;
get portraitContactBadgeColor => accent;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

69
lib/themes/neon2.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'opaque.dart';
const neon2_theme = "neon2";
OpaqueThemeType GetNeon2Theme(String mode) {
if (mode == mode_dark) {
return Neon2Dark();
} else {
return Neon2Light();
}
}
class Neon2Dark extends CwtchDark {
static final Color background = Color(0xFF290826);
static final Color header = Color(0xFF290826);
static final Color userBubble = Color(0xFFA604FE);
static final Color peerBubble = Color(0xFF03AD00);
static final Color font = Color(0xFFFFFFFF);
static final Color settings = Color(0xFFFFFDFF);
static final Color accent = Color(0xFFA604FE);
get theme => neon2_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get mainTextColor => font; //whiteishPurple;
get defaultButtonColor => accent; //hotPink;
get textfieldHintColor => mainTextColor; //TODO pick
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class Neon2Light extends CwtchLight {
static final Color background = Color(0xFFFFFDFF);
static final Color header = Color(0xFFD8C7E1);
static final Color userBubble = Color(0xFFD8C7E1);
static final Color peerBubble = Color(0xFF80E27E);
static final Color font = Color(0xFF290826);
static final Color settings = Color(0xFF290826);
static final Color accent = Color(0xFFA604FE);
get theme => neon2_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple;
get mainTextColor => settings;
get defaultButtonColor => accent; // hotPink;
get textfieldHintColor => font; //TODO pick
get scrollbarDefaultColor => accent;
get portraitContactBadgeColor => accent;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

203
lib/themes/opaque.dart Normal file
View File

@ -0,0 +1,203 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:cwtch/themes/mermaid.dart';
import 'package:cwtch/themes/neon1.dart';
import 'package:cwtch/themes/pumpkin.dart';
import 'package:cwtch/themes/vampire.dart';
import 'package:cwtch/themes/witch.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
import 'ghost.dart';
import 'midnight.dart';
import 'neon2.dart';
const mode_light = "light";
const mode_dark = "dark";
final themes = {
cwtch_theme: {mode_light: CwtchLight(), mode_dark: CwtchDark()},
ghost_theme: {mode_light: GhostLight(), mode_dark: GhostDark()},
mermaid_theme: {mode_light: MermaidLight(), mode_dark: MermaidDark()},
midnight_theme: {mode_light: MidnightLight(), mode_dark: MidnightDark()},
neon1_theme: {mode_light: Neon1Light(), mode_dark: Neon1Dark()},
neon2_theme: {mode_light: Neon2Light(), mode_dark: Neon2Dark()},
pumpkin_theme: {mode_light: PumpkinLight(), mode_dark: PumpkinDark()},
witch_theme: {mode_light: WitchLight(), mode_dark: WitchDark()},
vampire_theme: {mode_light: VampireLight(), mode_dark: VampireDark()},
};
OpaqueThemeType getTheme(String themeId, String mode) {
if (themeId == "") {
themeId = cwtch_theme;
}
if (themeId == mode_light) {
themeId = cwtch_theme;
mode = mode_light;
}
if (themeId == mode_dark) {
themeId = cwtch_theme;
mode = mode_dark;
}
var theme = themes[themeId]?[mode];
return theme ?? CwtchDark();
}
Color lighten(Color color, [double amount = 0.15]) {
final hsl = HSLColor.fromColor(color);
final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
return hslLight.toColor();
}
Color darken(Color color, [double amount = 0.15]) {
final hsl = HSLColor.fromColor(color);
final hslDarken = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDarken.toColor();
}
abstract class OpaqueThemeType {
static final Color red = Color(0xFFFF0000);
get theme => "dummy";
get mode => mode_light;
// Main screen background color (message pane, item rows)
get backgroundMainColor => red;
// pane colors (settings)
get backgroundPaneColor => red;
get topbarColor => red;
get mainTextColor => red;
// pressed row, offline heart
get hilightElementColor => red;
// Selected Row
get backgroundHilightElementColor => red;
// Faded text color for suggestions in textfields
// Todo: implement way more places
get sendHintTextColor => red;
get defaultButtonColor => red;
get defaultButtonActiveColor => /*mode == mode_light ? darken(defaultButtonColor) :*/ lighten(defaultButtonColor);
get defaultButtonTextColor => red;
get defaultButtonDisabledColor => red;
get textfieldBackgroundColor => red;
get textfieldBorderColor => red;
get textfieldHintColor => red;
get textfieldErrorColor => red;
get scrollbarDefaultColor => red;
get portraitBackgroundColor => red;
get portraitOnlineBorderColor => red;
get portraitOfflineBorderColor => red;
get portraitBlockedBorderColor => red;
get portraitBlockedTextColor => red;
get portraitContactBadgeColor => red;
get portraitContactBadgeTextColor => red;
get portraitProfileBadgeColor => red;
get portraitProfileBadgeTextColor => red;
// dropshaddpow
// todo: probably should not be reply icon color in messagerow
get dropShadowColor => red;
get toolbarIconColor => red;
get messageFromMeBackgroundColor => red;
get messageFromMeTextColor => red;
get messageFromOtherBackgroundColor => red;
get messageFromOtherTextColor => red;
// Sizes
double contactOnionTextSize() {
return 18;
}
}
ThemeData mkThemeData(Settings opaque) {
return ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
primarySwatch: Colors.red,
primaryIconTheme: IconThemeData(
color: opaque.current().mainTextColor,
),
primaryColor: opaque.current().backgroundMainColor,
canvasColor: opaque.current().backgroundPaneColor,
backgroundColor: opaque.current().backgroundMainColor,
highlightColor: opaque.current().hilightElementColor,
iconTheme: IconThemeData(
color: opaque.current().toolbarIconColor,
),
cardColor: opaque.current().backgroundMainColor,
appBarTheme: AppBarTheme(
backgroundColor: opaque.current().topbarColor,
iconTheme: IconThemeData(
color: opaque.current().mainTextColor,
),
titleTextStyle: TextStyle(
color: opaque.current().mainTextColor,
),
actionsIconTheme: IconThemeData(
color: opaque.current().mainTextColor,
)),
//bottomNavigationBarTheme: BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed, backgroundColor: opaque.current().backgroundHilightElementColor), // Can't determine current use
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
),
hintColor: opaque.current().textfieldHintColor,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) => states.contains(MaterialState.disabled) ? opaque.current().defaultButtonDisabledColor : opaque.current().defaultButtonColor),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor),
overlayColor: MaterialStateProperty.resolveWith((states) => (states.contains(MaterialState.pressed) && states.contains(MaterialState.hovered))
? opaque.current().defaultButtonActiveColor
: states.contains(MaterialState.disabled)
? opaque.current().defaultButtonDisabledColor
: null),
enableFeedback: true,
splashFactory: InkRipple.splashFactory,
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
)),
),
),
scrollbarTheme: ScrollbarThemeData(isAlwaysShown: false, thumbColor: MaterialStateProperty.all(opaque.current().scrollbarDefaultColor)),
tabBarTheme: TabBarTheme(indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor))),
dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor, titleTextStyle: TextStyle(color: opaque.current().mainTextColor), contentTextStyle: TextStyle(color: opaque.current().mainTextColor)),
textTheme: TextTheme(
headline1: TextStyle(color: opaque.current().mainTextColor),
headline2: TextStyle(color: opaque.current().mainTextColor),
headline3: TextStyle(color: opaque.current().mainTextColor),
headline4: TextStyle(color: opaque.current().mainTextColor),
headline5: TextStyle(color: opaque.current().mainTextColor),
headline6: TextStyle(color: opaque.current().mainTextColor),
bodyText1: TextStyle(color: opaque.current().mainTextColor),
bodyText2: TextStyle(color: opaque.current().mainTextColor),
subtitle1: TextStyle(color: opaque.current().mainTextColor),
subtitle2: TextStyle(color: opaque.current().mainTextColor),
caption: TextStyle(color: opaque.current().mainTextColor),
button: TextStyle(color: opaque.current().mainTextColor),
overline: TextStyle(color: opaque.current().mainTextColor)),
switchTheme: SwitchThemeData(
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor),
thumbColor: MaterialStateProperty.all(opaque.current().mainTextColor),
trackColor: MaterialStateProperty.all(opaque.current().dropShadowColor),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: opaque.current().defaultButtonColor, hoverColor: opaque.current().defaultButtonActiveColor, enableFeedback: true, splashColor: opaque.current().defaultButtonActiveColor),
textSelectionTheme: TextSelectionThemeData(
cursorColor: opaque.current().defaultButtonActiveColor, selectionColor: opaque.current().defaultButtonActiveColor, selectionHandleColor: opaque.current().defaultButtonActiveColor),
);
}

69
lib/themes/pumpkin.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'opaque.dart';
const pumpkin_theme = "pumpkin";
OpaqueThemeType GetPumpkinTheme(String mode) {
if (mode == mode_dark) {
return PumpkinDark();
} else {
return PumpkinLight();
}
}
class PumpkinDark extends CwtchDark {
static final Color background = Color(0xFF281831);
static final Color header = Color(0xFF281831);
static final Color userBubble = Color(0xFFB53D00);
static final Color peerBubble = Color(0xFF422850);
static final Color font = Color(0xFFFFFFFF);
static final Color settings = Color(0xFFFFFBF6);
static final Color accent = Color(0xFF8E64A5);
get theme => pumpkin_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get mainTextColor => font; //whiteishPurple;
get defaultButtonColor => accent; //hotPink;
get textfieldHintColor => mainTextColor; //TODO pick
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class PumpkinLight extends CwtchLight {
static final Color background = Color(0xFFFFFBF6);
static final Color header = Color(0xFFFF9800);
static final Color userBubble = Color(0xFFFF9800);
static final Color peerBubble = Color(0xFFD8C7E1);
static final Color font = Color(0xFF281831);
static final Color settings = Color(0xFF281831);
static final Color accent = Color(0xFF8E64A5);
get theme => pumpkin_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple;
get mainTextColor => settings;
get defaultButtonColor => accent; // hotPink;
get textfieldHintColor => font; //TODO pick
get scrollbarDefaultColor => accent;
get portraitContactBadgeColor => accent;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

69
lib/themes/vampire.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'opaque.dart';
const vampire_theme = "vampire";
OpaqueThemeType GetVampireTheme(String mode) {
if (mode == mode_dark) {
return VampireDark();
} else {
return VampireLight();
}
}
class VampireDark extends CwtchDark {
static final Color background = Color(0xFF281831);
static final Color header = Color(0xFF281831);
static final Color userBubble = Color(0xFF9A1218);
static final Color peerBubble = Color(0xFF422850);
static final Color font = Color(0xFFFFFFFF);
static final Color settings = Color(0xFFFDFFFD);
static final Color accent = Color(0xFF8E64A5);
get theme => vampire_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get mainTextColor => font; //whiteishPurple;
get defaultButtonColor => accent; //hotPink;
get textfieldHintColor => mainTextColor; //TODO pick
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class VampireLight extends CwtchLight {
static final Color background = Color(0xFFFFFDFD);
static final Color header = Color(0xFFD8C7E1);
static final Color userBubble = Color(0xFFD8C7E1);
static final Color peerBubble = Color(0xFFFFEBEE);
static final Color font = Color(0xFF281831);
static final Color settings = Color(0xFF281831);
static final Color accent = Color(0xFF8E64A5);
get theme => vampire_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple;
get mainTextColor => settings;
get defaultButtonColor => accent; // hotPink;
get textfieldHintColor => font; //TODO pick
get scrollbarDefaultColor => accent;
get portraitContactBadgeColor => accent;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

69
lib/themes/witch.dart Normal file
View File

@ -0,0 +1,69 @@
import 'dart:ui';
import 'dart:core';
import 'package:cwtch/themes/cwtch.dart';
import 'package:flutter/material.dart';
import 'opaque.dart';
const witch_theme = "witch";
OpaqueThemeType GetWitchTheme(String mode) {
if (mode == mode_dark) {
return WitchDark();
} else {
return WitchLight();
}
}
class WitchDark extends CwtchDark {
static final Color background = Color(0xFF0E1E0E);
static final Color header = Color(0xFF0E1E0E);
static final Color userBubble = Color(0xFF1B5E20);
static final Color peerBubble = Color(0xFF003300);
static final Color font = Color(0xFFFFFFFF);
static final Color settings = Color(0xFFFDFFFD);
static final Color accent = Color(0xFFD20070);
get theme => witch_theme;
get mode => mode_dark;
get backgroundMainColor => background; // darkGreyPurple;
get backgroundPaneColor => header; //darkGreyPurple;
get topbarColor => header; //darkGreyPurple;
get mainTextColor => font; //whiteishPurple;
get defaultButtonColor => accent; //hotPink;
get textfieldHintColor => mainTextColor; //TODO pick
get toolbarIconColor => settings; //whiteishPurple;
get messageFromMeBackgroundColor => userBubble; // mauvePurple;
get messageFromMeTextColor => font; //whiteishPurple;
get messageFromOtherBackgroundColor => peerBubble; //deepPurple;
get messageFromOtherTextColor => font; //whiteishPurple;
}
class WitchLight extends CwtchLight {
static final Color background = Color(0xFFFDFFFD);
static final Color header = Color(0xFF80E27E);
static final Color userBubble = Color(0xFF80E27E);
static final Color peerBubble = Color(0xFFE8F5E9);
static final Color font = Color(0xFF0E1E0E);
static final Color settings = Color(0xFF0E1E0E);
static final Color accent = Color(0xFFD20070);
get theme => witch_theme;
get mode => mode_light;
get backgroundMainColor => background; //whitePurple;
get backgroundPaneColor => background; //whitePurple;
get topbarColor => header; //softPurple;
get mainTextColor => settings;
get defaultButtonColor => accent; // hotPink;
get textfieldHintColor => font; //TODO pick
get scrollbarDefaultColor => accent;
get portraitContactBadgeColor => accent;
get toolbarIconColor => settings; //darkPurple;
get messageFromMeBackgroundColor => userBubble; //brightPurple;
get messageFromMeTextColor => font; //mainTextColor;
get messageFromOtherBackgroundColor => peerBubble; //purple;
get messageFromOtherTextColor => font; //darkPurple;
}

View File

@ -18,6 +18,9 @@ class TorStatus extends ChangeNotifier {
progress = new_progress;
status = new_status;
if (new_progress != 100) {
status = "$new_progress% - $new_status";
}
notifyListeners();
}

View File

@ -4,7 +4,7 @@ import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/errorHandler.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/settings.dart';
import 'package:cwtch/widgets/buttontextfield.dart';
import 'package:cwtch/widgets/cwtchlabel.dart';
@ -52,22 +52,26 @@ class _AddContactViewState extends State<AddContactView> {
/// We display a different number of tabs depending on the experiment setup
bool groupsEnabled = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
return Consumer<ErrorHandler>(builder: (context, globalErrorHandler, child) {
return DefaultTabController(
length: groupsEnabled ? 2 : 1,
child: Column(children: [
(groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()),
Expanded(
child: TabBarView(
children: (groupsEnabled
? [
addPeerTab(),
addGroupTab(),
]
: [addPeerTab()]),
)),
]));
});
return Scrollbar(
isAlwaysShown: true,
child: SingleChildScrollView(
clipBehavior: Clip.antiAlias,
child: Consumer<ErrorHandler>(builder: (context, globalErrorHandler, child) {
return DefaultTabController(
length: groupsEnabled ? 2 : 1,
child: Column(children: [
(groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()),
Expanded(
child: TabBarView(
children: (groupsEnabled
? [
addPeerTab(),
addGroupTab(),
]
: [addPeerTab()]),
)),
]));
})));
}
void _copyOnion() {
@ -163,7 +167,7 @@ class _AddContactViewState extends State<AddContactView> {
}
});
},
labelText: '',
hintText: '',
)
])));
}
@ -197,11 +201,15 @@ class _AddContactViewState extends State<AddContactView> {
},
isExpanded: true, // magic property
value: server,
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
items: Provider.of<ProfileInfoState>(context)
.serverList
.servers
.where((serverInfo) => serverInfo.status == "Synced")
.map<DropdownMenuItem<String>>((RemoteServerInfoState serverInfo) {
return DropdownMenuItem<String>(
value: serverInfo.onion,
child: Text(
serverInfo.onion,
serverInfo.description,
overflow: TextOverflow.ellipsis,
),
);
@ -215,7 +223,7 @@ class _AddContactViewState extends State<AddContactView> {
),
CwtchTextField(
controller: ctrlrGroupName,
labelText: AppLocalizations.of(context)!.groupNameLabel,
hintText: AppLocalizations.of(context)!.groupNameLabel,
onChanged: (newValue) {},
validator: (value) {},
),
@ -240,8 +248,8 @@ class _AddContactViewState extends State<AddContactView> {
/// TODO Manage Servers Tab
Widget manageServersTab() {
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) {
return ChangeNotifierProvider<ServerInfoState>.value(
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((RemoteServerInfoState server) {
return ChangeNotifierProvider<RemoteServerInfoState>.value(
value: server,
child: ListTile(
title: Text(server.onion),

View File

@ -1,6 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/model.dart';
@ -15,7 +18,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../cwtch_icons_icons.dart';
import '../errorHandler.dart';
import '../main.dart';
import '../opaque.dart';
import '../themes/opaque.dart';
import '../settings.dart';
class AddEditProfileView extends StatefulWidget {
@ -90,7 +93,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
imagePath: Provider.of<ProfileInfoState>(context).imagePath,
diameter: 120,
maskOut: false,
border: theme.theme.portraitOnlineBorderColor(),
border: theme.theme.portraitOnlineBorderColor,
badgeTextColor: Colors.red,
badgeColor: Colors.red,
)
@ -103,10 +106,10 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
CwtchTextField(
controller: ctrlrNick,
autofocus: false,
labelText: AppLocalizations.of(context)!.yourDisplayName,
hintText: AppLocalizations.of(context)!.yourDisplayName,
validator: (value) {
if (value.isEmpty) {
return "Please enter a display name";
return AppLocalizations.of(context)!.displayNameTooltip;
}
return null;
},
@ -136,17 +139,31 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
// We only allow setting password types on profile creation
Visibility(
visible: Provider.of<ProfileInfoState>(context).onion.isEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
child: SizedBox(
height: 20,
)),
Visibility(
visible: Provider.of<ProfileInfoState>(context).onion.isEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Checkbox(
value: usePassword,
fillColor: MaterialStateProperty.all(theme.current().defaultButtonColor()),
activeColor: theme.current().defaultButtonActiveColor(),
fillColor: MaterialStateProperty.all(theme.current().defaultButtonColor),
activeColor: theme.current().defaultButtonActiveColor,
onChanged: _handleSwitchPassword,
),
Text(
AppLocalizations.of(context)!.radioUsePassword,
style: TextStyle(color: theme.current().mainTextColor()),
style: TextStyle(color: theme.current().mainTextColor),
),
SizedBox(
height: 20,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
usePassword ? AppLocalizations.of(context)!.encryptedProfileDescription : AppLocalizations.of(context)!.plainProfileDescription,
textAlign: TextAlign.center,
))
])),
SizedBox(
height: 20,
@ -249,7 +266,6 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
onPressed: () {
showAlertDialog(context);
},
style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()),
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn),
))
@ -264,7 +280,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
// TODO Toast
}
void _createPressed() {
void _createPressed() async {
// This will run all the validations in the form including
// checking that display name is not empty, and an actual check that the passwords
// match (and are provided if the user has requested an encrypted profile).
@ -274,41 +290,44 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text);
Navigator.of(context).pop();
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, "be gay do crime");
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, DefaultPassword);
Navigator.of(context).pop();
}
} else {
// Profile Editing
if (ctrlrPass.value.text.isEmpty) {
// Don't update password, only update name
final event = {
"EventType": "SetAttribute",
"Data": {"Key": "public.name", "Data": ctrlrNick.value.text}
};
final json = jsonEncode(event);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, json);
Provider.of<ProfileInfoState>(context, listen: false).nickname = ctrlrNick.value.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
Navigator.of(context).pop();
} else {
// At this points passwords have been validated to be the same and not empty
// Update both password and name, even if name hasn't been changed...
final updateNameEvent = {
"EventType": "SetAttribute",
"Data": {"Key": "public.name", "Data": ctrlrNick.value.text}
};
final updateNameEventJson = jsonEncode(updateNameEvent);
var profile = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<ProfileInfoState>(context, listen: false).nickname = ctrlrNick.value.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(profile, "profile.name", ctrlrNick.value.text);
Provider.of<FlwtchState>(context, listen: false).cwtch.ChangePassword(profile, ctrlrOldPass.text, ctrlrPass.text, ctrlrPass2.text);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, updateNameEventJson);
final updatePasswordEvent = {
"EventType": "ChangePassword",
"Data": {"Password": ctrlrOldPass.text, "NewPassword": ctrlrPass.text}
};
final updatePasswordEventJson = jsonEncode(updatePasswordEvent);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, updatePasswordEventJson);
Navigator.of(context).pop();
EnvironmentConfig.debugLog("waiting for change password response");
Future.delayed(const Duration(milliseconds: 500), () {
if (globalErrorHandler.changePasswordError) {
// TODO: This isn't ideal, but because onChange can be fired during this future check
// and because the context can change after being popped we have this kind of double assertion...
// There is probably a better pattern to handle this...
if (AppLocalizations.of(context) != null) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.passwordChangeError));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
return;
}
}
}).whenComplete(() {
if (globalErrorHandler.explicitChangePasswordSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.newPassword));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context);
return; // otherwise round and round we go...
}
});
}
}
}
@ -316,13 +335,13 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
Widget cancelButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
Widget continueButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
onPressed: () {
var onion = Provider.of<ProfileInfoState>(context, listen: false).onion;

View File

@ -0,0 +1,388 @@
import 'dart:convert';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/cwtchlabel.dart';
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:cwtch/widgets/textfield.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../errorHandler.dart';
import '../main.dart';
import '../config.dart';
/// Pane to add or edit a server
class AddEditServerView extends StatefulWidget {
const AddEditServerView();
@override
_AddEditServerViewState createState() => _AddEditServerViewState();
}
class _AddEditServerViewState extends State<AddEditServerView> {
final _formKey = GlobalKey<FormState>();
final ctrlrDesc = TextEditingController(text: "");
final ctrlrOldPass = TextEditingController(text: "");
final ctrlrPass = TextEditingController(text: "");
final ctrlrPass2 = TextEditingController(text: "");
final ctrlrOnion = TextEditingController(text: "");
late bool usePassword;
@override
void initState() {
super.initState();
var serverInfoState = Provider.of<ServerInfoState>(context, listen: false);
ctrlrOnion.text = serverInfoState.onion;
usePassword = serverInfoState.isEncrypted;
if (serverInfoState.description.isNotEmpty) {
ctrlrDesc.text = serverInfoState.description;
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle),
),
body: _buildSettingsList(),
);
}
void _handleSwitchPassword(bool? value) {
setState(() {
usePassword = value!;
});
}
Widget _buildSettingsList() {
return Consumer2<ServerInfoState, Settings>(builder: (context, serverInfoState, settings, child) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
return Scrollbar(
isAlwaysShown: true,
child: SingleChildScrollView(
clipBehavior: Clip.antiAlias,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Form(
key: _formKey,
child: Container(
margin: EdgeInsets.fromLTRB(30, 5, 30, 10),
padding: EdgeInsets.fromLTRB(20, 5, 20, 10),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// Onion
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [CwtchLabel(label: AppLocalizations.of(context)!.serverAddress), SelectableText(serverInfoState.onion)])),
// Description
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
Text(AppLocalizations.of(context)!.serverDescriptionDescription),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrDesc,
hintText: AppLocalizations.of(context)!.fieldDescriptionLabel,
autofocus: false,
)
]),
SizedBox(
height: 20,
),
// Enabled
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: SwitchListTile(
title: Text(AppLocalizations.of(context)!.serverEnabled, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.serverEnabledDescription),
value: serverInfoState.running,
onChanged: (bool value) {
serverInfoState.setRunning(value);
if (value) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchServer(serverInfoState.onion);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.StopServer(serverInfoState.onion);
}
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor),
)),
// Auto start
SwitchListTile(
title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription),
value: serverInfoState.autoStart,
onChanged: (bool value) {
serverInfoState.setAutostart(value);
if (!serverInfoState.onion.isEmpty) {
Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false");
}
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor),
),
// metrics
Visibility(
visible: serverInfoState.onion.isNotEmpty && serverInfoState.running,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
Text(AppLocalizations.of(context)!.serverMetricsLabel, style: Provider.of<FlwtchState>(context).biggerFont),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(AppLocalizations.of(context)!.serverTotalMessagesLabel),
]),
Text(serverInfoState.totalMessages.toString())
]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(AppLocalizations.of(context)!.serverConnectionsLabel),
]),
Text(serverInfoState.connections.toString())
]),
])),
// ***** Password *****
// use password toggle
Visibility(
visible: serverInfoState.onion.isEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
SizedBox(
height: 20,
),
Checkbox(
value: usePassword,
fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor),
activeColor: settings.current().defaultButtonActiveColor,
onChanged: _handleSwitchPassword,
),
Text(
AppLocalizations.of(context)!.radioUsePassword,
style: TextStyle(color: settings.current().mainTextColor),
),
SizedBox(
height: 20,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
usePassword ? AppLocalizations.of(context)!.encryptedServerDescription : AppLocalizations.of(context)!.plainServerDescription,
textAlign: TextAlign.center,
)),
SizedBox(
height: 20,
),
])),
// current password
Visibility(
visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrOldPass,
autoFillHints: [AutofillHints.newPassword],
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.isEncrypted && serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (Provider.of<ErrorHandler>(context).deletedServerError == true) {
return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer;
}
return null;
},
),
SizedBox(
height: 20,
),
])),
// new passwords 1 & 2
Visibility(
// Currently we don't support password change for servers so also gate this on Add server, when ready to support changing password remove the onion.isEmpty check
visible: serverInfoState.onion.isEmpty && usePassword,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPass,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass2.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
},
),
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPass2,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
}),
]),
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed,
child: Text(
serverInfoState.onion.isEmpty ? AppLocalizations.of(context)!.addServerTitle : AppLocalizations.of(context)!.saveServerButton,
textAlign: TextAlign.center,
),
),
),
],
),
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn),
))
]))
// ***** END Password *****
]))))));
});
});
}
void _createPressed() {
// This will run all the validations in the form including
// checking that display name is not empty, and an actual check that the passwords
// match (and are provided if the user has requested an encrypted profile).
if (_formKey.currentState!.validate()) {
if (usePassword) {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
}
Navigator.of(context).pop();
}
}
void _savePressed() {
var server = Provider.of<ServerInfoState>(context, listen: false);
Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text);
server.setDescription(ctrlrDesc.text);
if (_formKey.currentState!.validate()) {
// TODO support change password
}
Navigator.of(context).pop();
}
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn),
onPressed: () {
var onion = Provider.of<ServerInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteServer(onion, Provider.of<ServerInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword);
Future.delayed(
const Duration(milliseconds: 500),
() {
if (globalErrorHandler.deletedServerSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteServerSuccess + ":" + onion));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.of(context).popUntil((route) => route.settings.name == "servers"); // dismiss dialog
} else {
Navigator.of(context).pop();
}
},
);
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}

View File

@ -1,10 +1,11 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/views/profileserversview.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/views/torstatusview.dart';
import 'package:cwtch/widgets/contactrow.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:cwtch/widgets/textfield.dart';
import 'package:cwtch/widgets/tor_icon.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../settings.dart';
@ -12,6 +13,8 @@ import 'addcontactview.dart';
import '../model.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'messageview.dart';
class ContactsView extends StatefulWidget {
const ContactsView({Key? key}) : super(key: key);
@ -19,6 +22,41 @@ class ContactsView extends StatefulWidget {
_ContactsViewState createState() => _ContactsViewState();
}
// selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, int handle) {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).initialScrollIndex = initialIndex;
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
Provider.of<AppState>(context, listen: false).selectedIndex = null;
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
}
void _pushMessageView(BuildContext context, int handle) {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
// assert we have an actual profile...
// We need to listen for updates to the profile in order to update things like invitation message bubbles.
var profile = Provider.of<FlwtchState>(builderContext).profs.getProfile(profileOnion)!;
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
],
builder: (context, child) => MessageView(),
);
},
),
);
}
class _ContactsViewState extends State<ContactsView> {
late TextEditingController ctrlrFilter;
bool showSearchBar = false;
@ -40,7 +78,7 @@ class _ContactsViewState extends State<ContactsView> {
ProfileImage(
imagePath: Provider.of<ProfileInfoState>(context).imagePath,
diameter: 42,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor(),
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
badgeTextColor: Colors.red,
badgeColor: Colors.red,
),
@ -49,20 +87,9 @@ class _ContactsViewState extends State<ContactsView> {
),
Expanded(
child: Text("%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname).replaceAll("%2", AppLocalizations.of(context)!.titleManageContacts),
overflow: TextOverflow.ellipsis, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor()))), //todo
overflow: TextOverflow.ellipsis, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor))),
])),
actions: [
IconButton(icon: TorIcon(), onPressed: _pushTorStatus),
IconButton(
// need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),
onPressed: () {
Provider.of<ContactListState>(context, listen: false).filter = "";
setState(() {
showSearchBar = !showSearchBar;
});
})
],
actions: getActions(context),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddContact,
@ -72,10 +99,47 @@ class _ContactsViewState extends State<ContactsView> {
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList());
}
List<Widget> getActions(context) {
var actions = List<Widget>.empty(growable: true);
if (Provider.of<Settings>(context).blockUnknownConnections) {
actions.add(Tooltip(message: AppLocalizations.of(context)!.blockUnknownConnectionsEnabledDescription, child: Icon(CwtchIcons.block_unknown)));
}
// Copy profile onion
actions.add(IconButton(
icon: Icon(CwtchIcons.address_copy_2),
tooltip: AppLocalizations.of(context)!.copyAddress,
onPressed: () {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
}));
// Manage known Servers
if (Provider.of<Settings>(context, listen: false).isExperimentEnabled(TapirGroupsExperiment) || Provider.of<Settings>(context, listen: false).isExperimentEnabled(ServerManagementExperiment)) {
actions.add(IconButton(
icon: Icon(CwtchIcons.dns_24px),
tooltip: AppLocalizations.of(context)!.manageKnownServersButton,
onPressed: () {
_pushServers();
}));
}
// Search contacts
actions.add(IconButton(
// need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),
onPressed: () {
Provider.of<ContactListState>(context, listen: false).filter = "";
setState(() {
showSearchBar = !showSearchBar;
});
}));
return actions;
}
Widget _buildFilterable() {
Widget txtfield = CwtchTextField(
controller: ctrlrFilter,
labelText: AppLocalizations.of(context)!.search,
hintText: AppLocalizations.of(context)!.search,
onChanged: (newVal) {
Provider.of<ContactListState>(context, listen: false).filter = newVal;
},
@ -107,12 +171,13 @@ class _ContactsViewState extends State<ContactsView> {
));
}
void _pushTorStatus() {
void _pushServers() {
var profile = Provider.of<ProfileInfoState>(context);
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
child: TorStatusView(),
providers: [ChangeNotifierProvider(create: (context) => profile), Provider.value(value: Provider.of<FlwtchState>(context))],
child: ProfileServersView(),
);
},
));

View File

@ -29,12 +29,18 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
Flexible(
flex: cols[1],
child: flwtch.selectedConversation == null
? Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)))
? Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor,
child: Card(
margin: EdgeInsets.all(0.0),
shape: new RoundedRectangleBorder(side: new BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonColor, width: 4.0), borderRadius: BorderRadius.circular(4.0)),
child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst))))
: //dev
MultiProvider(providers: [
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)!),
], child: Container(child: MessageView())),
ChangeNotifierProvider.value(
value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", -1, "")),
], child: Container(key: Key(flwtch.selectedConversation!.toString()), child: MessageView())),
),
],
);

View File

@ -1,5 +1,18 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/folderpicker.dart';
import 'package:cwtch/themes/cwtch.dart';
import 'package:cwtch/themes/ghost.dart';
import 'package:cwtch/themes/mermaid.dart';
import 'package:cwtch/themes/midnight.dart';
import 'package:cwtch/themes/neon1.dart';
import 'package:cwtch/themes/neon2.dart';
import 'package:cwtch/themes/opaque.dart';
import 'package:cwtch/themes/pumpkin.dart';
import 'package:cwtch/themes/vampire.dart';
import 'package:cwtch/themes/witch.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
@ -34,6 +47,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
Widget _buildSettingsList() {
return Consumer<Settings>(builder: (context, settings, child) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
var appIcon = Icon(Icons.info, color: settings.current().mainTextColor);
return Scrollbar(
isAlwaysShown: true,
child: SingleChildScrollView(
@ -44,8 +58,8 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
),
child: Column(children: [
ListTile(
title: Text(AppLocalizations.of(context)!.settingLanguage, style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(CwtchIcons.change_language, color: settings.current().mainTextColor()),
title: Text(AppLocalizations.of(context)!.settingLanguage, style: TextStyle(color: settings.current().mainTextColor)),
leading: Icon(CwtchIcons.change_language, color: settings.current().mainTextColor),
trailing: DropdownButton(
value: Provider.of<Settings>(context).locale.languageCode,
onChanged: (String? newValue) {
@ -61,25 +75,43 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
);
}).toList())),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingTheme, style: TextStyle(color: settings.current().mainTextColor())),
value: settings.current().identifier() == "light",
title: Text(AppLocalizations.of(context)!.settingTheme, style: TextStyle(color: settings.current().mainTextColor)),
value: settings.current().mode == mode_light,
onChanged: (bool value) {
if (value) {
settings.setLight();
settings.setTheme(settings.theme.theme, mode_light);
} else {
settings.setDark();
settings.setTheme(settings.theme.theme, mode_dark);
}
// Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),
),
ListTile(
title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait, style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
title: Text(AppLocalizations.of(context)!.themeColorLabel),
trailing: DropdownButton<String>(
value: Provider.of<Settings>(context).theme.theme,
onChanged: (String? newValue) {
setState(() {
settings.setTheme(newValue!, settings.theme.mode);
saveSettings(context);
});
},
items: themes.keys.map<DropdownMenuItem<String>>((String themeId) {
return DropdownMenuItem<String>(
value: themeId,
child: Text(getThemeName(context, themeId)),
);
}).toList()),
leading: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),
),
ListTile(
title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait, style: TextStyle(color: settings.current().mainTextColor)),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor),
trailing: DropdownButton(
value: settings.uiColumnModePortrait.toString(),
onChanged: (String? newValue) {
@ -93,22 +125,33 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
);
}).toList())),
ListTile(
title: Text(AppLocalizations.of(context)!.settingUIColumnLandscape, style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: settings.uiColumnModeLandscape.toString(),
onChanged: (String? newValue) {
settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(true).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(Settings.uiColumnModeToString(value, context)),
);
}).toList())),
title: Text(
AppLocalizations.of(context)!.settingUIColumnLandscape,
textWidthBasis: TextWidthBasis.longestLine,
softWrap: true,
style: TextStyle(color: settings.current().mainTextColor),
),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor),
trailing: Container(
width: 200.0,
child: DropdownButton(
isExpanded: true,
value: settings.uiColumnModeLandscape.toString(),
onChanged: (String? newValue) {
settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(true).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(
Settings.uiColumnModeToString(value, context),
overflow: TextOverflow.ellipsis,
),
);
}).toList()))),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.blockUnknownLabel, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.blockUnknownLabel, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.descriptionBlockUnknownConnections),
value: settings.blockUnknownConnections,
onChanged: (bool value) {
@ -121,12 +164,25 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.block_unknown, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.block_unknown, color: settings.current().mainTextColor),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.experimentsEnabled, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.streamerModeLabel, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.descriptionStreamerMode),
value: settings.streamerMode,
onChanged: (bool value) {
settings.setStreamerMode(value);
// Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.streamer_bunnymask, color: settings.current().mainTextColor),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.experimentsEnabled, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.descriptionExperiments),
value: settings.experimentsEnabled,
onChanged: (bool value) {
@ -138,16 +194,16 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.enable_experiments, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.enable_experiments, color: settings.current().mainTextColor),
),
Visibility(
visible: settings.experimentsEnabled,
child: Column(
children: [
SwitchListTile(
title: Text(AppLocalizations.of(context)!.enableGroups, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.enableGroups, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.descriptionExperimentsGroups),
value: settings.isExperimentEnabled(TapirGroupsExperiment),
onChanged: (bool value) {
@ -159,19 +215,109 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor),
),
Visibility(
visible: !Platform.isAndroid && !Platform.isIOS,
child: SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.settingServersDescription),
value: settings.isExperimentEnabled(ServerManagementExperiment),
onChanged: (bool value) {
Provider.of<ServerListState>(context, listen: false).clear();
if (value) {
settings.enableExperiment(ServerManagementExperiment);
} else {
settings.disableExperiment(ServerManagementExperiment);
}
// Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.dns_24px, color: settings.current().mainTextColor),
)),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingFileSharing, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.descriptionFileSharing),
value: settings.isExperimentEnabled(FileSharingExperiment),
onChanged: (bool value) {
if (value) {
settings.enableExperiment(FileSharingExperiment);
} else {
settings.disableExperiment(FileSharingExperiment);
}
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor),
),
Visibility(
visible: settings.isExperimentEnabled(FileSharingExperiment),
child: Column(children: [
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingImagePreviews, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.settingImagePreviewsDescription),
value: settings.isExperimentEnabled(ImagePreviewsExperiment),
onChanged: (bool value) {
if (value) {
settings.enableExperiment(ImagePreviewsExperiment);
settings.downloadPath = Provider.of<FlwtchState>(context, listen: false).cwtch.defaultDownloadPath();
} else {
settings.disableExperiment(ImagePreviewsExperiment);
}
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor),
),
Visibility(
visible: settings.isExperimentEnabled(ImagePreviewsExperiment) && !Platform.isAndroid,
child: CwtchFolderPicker(
label: AppLocalizations.of(context)!.settingDownloadFolder,
initialValue: settings.downloadPath,
onSave: (newVal) {
settings.downloadPath = newVal;
saveSettings(context);
},
),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.enableExperimentClickableLinks, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.experimentClickableLinksDescription),
value: settings.isExperimentEnabled(ClickableLinksExperiment),
onChanged: (bool value) {
if (value) {
settings.enableExperiment(ClickableLinksExperiment);
} else {
settings.disableExperiment(ClickableLinksExperiment);
}
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.link, color: settings.current().mainTextColor),
),
]),
),
],
)),
AboutListTile(
icon: Icon(Icons.info, color: settings.current().mainTextColor()),
applicationIcon: Padding(padding: EdgeInsets.all(5), child: Icon(CwtchIcons.cwtch_knott)),
applicationName: "Cwtch (Flutter UI)",
applicationVersion: AppLocalizations.of(context)!.versionBuilddate.replaceAll("%1", EnvironmentConfig.BUILD_VER).replaceAll("%2", EnvironmentConfig.BUILD_DATE),
applicationLegalese: '\u{a9} 2021 Open Privacy Research Society',
),
icon: appIcon,
applicationIcon: Padding(padding: EdgeInsets.all(5), child: Icon(CwtchIcons.cwtch_knott)),
applicationName: "Cwtch (Flutter UI)",
applicationLegalese: '\u{a9} 2021 Open Privacy Research Society',
aboutBoxChildren: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(24.0 + 10.0 + (appIcon.size ?? 24.0), 16.0, 0.0, 0.0),
// About has 24 padding (ln 389) and there appears to be another 10 of padding in the widget
child: SelectableText(AppLocalizations.of(context)!.versionBuilddate.replaceAll("%1", EnvironmentConfig.BUILD_VER).replaceAll("%2", EnvironmentConfig.BUILD_DATE)),
)
]),
]))));
});
});
@ -210,9 +356,37 @@ String getLanguageFull(context, String languageCode) {
if (languageCode == "pl") {
return AppLocalizations.of(context)!.localePl;
}
if (languageCode == "ru") {
return AppLocalizations.of(context)!.localeRU;
}
return languageCode;
}
/// Since we don't seem to able to dynamically pull translations, this function maps themes to their names
String getThemeName(context, String theme) {
switch (theme) {
case cwtch_theme:
return AppLocalizations.of(context)!.themeNameCwtch;
case ghost_theme:
return AppLocalizations.of(context)!.themeNameGhost;
case mermaid_theme:
return AppLocalizations.of(context)!.themeNameMermaid;
case midnight_theme:
return AppLocalizations.of(context)!.themeNameMidnight;
case neon1_theme:
return AppLocalizations.of(context)!.themeNameNeon1;
case neon2_theme:
return AppLocalizations.of(context)!.themeNameNeon2;
case pumpkin_theme:
return AppLocalizations.of(context)!.themeNamePumpkin;
case vampire_theme:
return AppLocalizations.of(context)!.themeNameVampire;
case witch_theme:
return AppLocalizations.of(context)!.themeNameWitch;
}
return theme;
}
/// Send an UpdateGlobalSettings to the Event Bus
saveSettings(context) {
var settings = Provider.of<Settings>(context, listen: false);

View File

@ -78,9 +78,9 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
readonly: false,
onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetGroupAttribute(profileOnion, handle, "local.name", ctrlrNick.text);
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, handle, "profile.name", ctrlrNick.text);
// todo translations
final snackBar = SnackBar(content: Text("Group Nickname changed successfully"));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
@ -101,7 +101,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
),
CwtchTextField(
controller: ctrlrGroupAddr,
labelText: '',
hintText: '',
validator: (value) {},
)
]),
@ -116,7 +116,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
CwtchTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).server),
validator: (value) {},
labelText: '',
hintText: '',
)
]),
@ -136,14 +136,40 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.leaveGroup,
message: AppLocalizations.of(context)!.archiveConversation,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle);
Future.delayed(Duration(milliseconds: 500), () {
Provider.of<AppState>(context, listen: false).selectedConversation = null;
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
});
},
icon: Icon(CwtchIcons.leave_group),
label: Text(AppLocalizations.of(context)!.leaveGroup),
))
icon: Icon(CwtchIcons.leave_chat),
label: Text(AppLocalizations.of(context)!.archiveConversation),
)),
SizedBox(
height: 20,
),
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
Tooltip(
message: AppLocalizations.of(context)!.leaveGroup,
child: TextButton.icon(
onPressed: () {
showAlertDialog(context);
},
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)),
icon: Icon(CwtchIcons.leave_group),
label: Text(
AppLocalizations.of(context)!.leaveGroup,
style: TextStyle(decoration: TextDecoration.underline),
),
))
])
])
])))));
});
@ -158,21 +184,24 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
Widget cancelButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
Widget continueButton = ElevatedButton(
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.LeaveGroup(profileOnion, handle);
var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<ProfileInfoState>(context, listen: false).contactList.removeContact(identifier);
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, identifier);
Future.delayed(Duration(milliseconds: 500), () {
Provider.of<AppState>(context, listen: false).selectedConversation = null;
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
});
},

View File

@ -1,11 +1,16 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/quotedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/views/peersettingsview.dart';
@ -13,6 +18,8 @@ import 'package:cwtch/widgets/DropdownContacts.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:path/path.dart' show basename;
import '../main.dart';
import '../model.dart';
@ -28,15 +35,36 @@ class MessageView extends StatefulWidget {
class _MessageViewState extends State<MessageView> {
final ctrlrCompose = TextEditingController();
final focusNode = FocusNode();
String selectedContact = "";
int selectedContact = -1;
ItemPositionsListener scrollListener = ItemPositionsListener.create();
ItemScrollController scrollController = ItemScrollController();
File? imagePreview;
// @override
// void didChangeDependencies() {
// super.didChangeDependencies();
// if (Provider.of<ContactInfoState>(context, listen: false).unreadMessages > 0) {
// Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0;
// }
// }
@override
void initState() {
scrollListener.itemPositions.addListener(() {
if (scrollListener.itemPositions.value.length != 0 &&
Provider.of<AppState>(context, listen: false).unreadMessagesBelow == true &&
scrollListener.itemPositions.value.any((element) => element.index == 0)) {
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
}
});
super.initState();
}
@override
void didChangeDependencies() {
var appState = Provider.of<AppState>(context, listen: false);
// using "8" because "# of messages that fit on one screen" isnt trivial to calculate at this point
if (appState.initialScrollIndex > 4 && appState.unreadMessagesBelow == false) {
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((timeStamp) {
appState.unreadMessagesBelow = true;
});
}
super.didChangeDependencies();
}
@override
void dispose() {
@ -47,10 +75,47 @@ class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) {
// After leaving a conversation the selected conversation is set to null...
if (Provider.of<ContactInfoState>(context).profileOnion == "") {
return Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)));
}
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
var appBarButtons = <Widget>[];
if (Provider.of<ContactInfoState>(context).isOnline()) {
if (showFileSharing) {
appBarButtons.add(IconButton(
icon: Icon(Icons.attach_file, size: 24),
tooltip: AppLocalizations.of(context)!.tooltipSendFile,
onPressed: (){_showFilePicker(context);},
));
}
appBarButtons.add(IconButton(
icon: Icon(CwtchIcons.send_invite, size: 24),
tooltip: AppLocalizations.of(context)!.sendInvite,
onPressed: () {
_modalSendInvitation(context);
}));
}
appBarButtons.add(IconButton(
icon: Provider.of<ContactInfoState>(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px),
tooltip: AppLocalizations.of(context)!.conversationSettings,
onPressed: _pushContactSettings));
var appState = Provider.of<AppState>(context);
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor,
floatingActionButton: appState.unreadMessagesBelow
? FloatingActionButton(
child: Icon(Icons.arrow_downward),
onPressed: () {
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
scrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
})
: null,
appBar: AppBar(
// setting leading to null makes it do the default behaviour; container() hides it
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null,
@ -58,32 +123,29 @@ class _MessageViewState extends State<MessageView> {
ProfileImage(
imagePath: Provider.of<ContactInfoState>(context).imagePath,
diameter: 42,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor(),
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
badgeTextColor: Colors.red,
badgeColor: Colors.red,
),
SizedBox(
width: 10,
),
Text(Provider.of<ContactInfoState>(context).nickname)
Expanded(
child: Text(
Provider.of<ContactInfoState>(context).nickname,
overflow: TextOverflow.ellipsis,
))
]),
actions: [
//IconButton(icon: Icon(Icons.chat), onPressed: _pushContactSettings),
//IconButton(icon: Icon(Icons.list), onPressed: _pushContactSettings),
//IconButton(icon: Icon(Icons.push_pin), onPressed: _pushContactSettings),
IconButton(
icon: Provider.of<ContactInfoState>(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px),
tooltip: AppLocalizations.of(context)!.conversationSettings,
onPressed: _pushContactSettings),
],
actions: appBarButtons,
),
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList()),
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(scrollController, scrollListener)),
bottomSheet: _buildComposeBox(),
));
}
Future<bool> _onWillPop() async {
Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0;
Provider.of<AppState>(context, listen: false).selectedConversation = null;
return true;
}
@ -110,7 +172,7 @@ class _MessageViewState extends State<MessageView> {
if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<AppState>(context, listen: false).selectedIndex != null) {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.GetMessage(Provider.of<AppState>(context, listen: false).selectedProfile!, Provider.of<AppState>(context, listen: false).selectedConversation!,
.GetMessageByID(Provider.of<AppState>(context, listen: false).selectedProfile!, Provider.of<AppState>(context, listen: false).selectedConversation!,
Provider.of<AppState>(context, listen: false).selectedIndex!)
.then((data) {
try {
@ -118,11 +180,11 @@ class _MessageViewState extends State<MessageView> {
var bytes1 = utf8.encode(messageWrapper["PeerID"] + messageWrapper['Message']);
var digest1 = sha256.convert(bytes1);
var contentHash = base64Encode(digest1.bytes);
var quotedMessage = "{\"quotedHash\":\"" + contentHash + "\",\"body\":\"" + ctrlrCompose.value.text + "\"}";
var quotedMessage = jsonEncode(QuotedMessageStructure(contentHash, ctrlrCompose.value.text));
ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
} catch (e) {}
Provider.of<AppState>(context, listen: false).selectedIndex = null;
_sendMessageHelper();
@ -131,7 +193,7 @@ class _MessageViewState extends State<MessageView> {
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
_sendMessageHelper();
}
}
@ -140,7 +202,14 @@ class _MessageViewState extends State<MessageView> {
void _sendInvitation([String? ignoredParam]) {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, this.selectedContact);
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact);
_sendMessageHelper();
}
void _sendFile(String filePath) {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath);
_sendMessageHelper();
}
@ -148,54 +217,54 @@ class _MessageViewState extends State<MessageView> {
ctrlrCompose.clear();
focusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 80), () {
Provider.of<ContactInfoState>(context, listen: false).totalMessages++;
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(Provider.of<ContactInfoState>(context, listen: false).identifier)?.bumpMessageCache();
Provider.of<ContactInfoState>(context, listen: false).newMarker++;
// Resort the contact list...
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).onion, DateTime.now());
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).identifier, DateTime.now());
});
}
Widget _buildComposeBox() {
bool isOffline = Provider.of<ContactInfoState>(context).isOnline() == false;
var composeBox = Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor(),
color: Provider.of<Settings>(context).theme.backgroundMainColor,
padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2),
height: 100,
child: Row(
children: <Widget>[
Expanded(
child: Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor()))),
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: handleKeyPress,
child: TextFormField(
key: Key('txtCompose'),
controller: ctrlrCompose,
focusNode: focusNode,
autofocus: !Platform.isAndroid,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
minLines: 1,
maxLines: null,
onFieldSubmitted: _sendMessage,
decoration: InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
enabled: true,
prefixIcon: IconButton(
icon: Icon(CwtchIcons.send_invite, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: AppLocalizations.of(context)!.sendInvite,
enableFeedback: true,
splashColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
hoverColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
onPressed: () => _modalSendInvitation(context)),
suffixIcon: IconButton(
icon: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: AppLocalizations.of(context)!.sendMessage,
onPressed: _sendMessage,
),
)))),
),
child: Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor))),
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: handleKeyPress,
child: Padding(
padding: EdgeInsets.all(8),
child: TextFormField(
key: Key('txtCompose'),
controller: ctrlrCompose,
focusNode: focusNode,
autofocus: !Platform.isAndroid,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
enableIMEPersonalizedLearning: false,
minLines: 1,
maxLines: null,
onFieldSubmitted: _sendMessage,
enabled: !isOffline,
decoration: InputDecoration(
hintText: isOffline ? "" : AppLocalizations.of(context)!.placeholderEnterMessage,
hintStyle: TextStyle(color: Provider.of<Settings>(context).theme.sendHintTextColor),
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
enabled: true,
suffixIcon: ElevatedButton(
child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
onPressed: isOffline ? null : _sendMessage,
))),
)))),
],
),
);
@ -203,7 +272,8 @@ class _MessageViewState extends State<MessageView> {
var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
var quoted = FutureBuilder(
future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!),
future:
messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!, byID: true),
builder: (context, snapshot) {
if (snapshot.hasData) {
var message = snapshot.data! as Message;
@ -211,20 +281,29 @@ class _MessageViewState extends State<MessageView> {
margin: EdgeInsets.all(5),
padding: EdgeInsets.all(5),
color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [
Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.reply, size: 32))),
Center(widthFactor: 1.0, child: message.getPreviewWidget(context)),
Center(
widthFactor: 1.0,
child: IconButton(
icon: Icon(Icons.highlight_remove),
tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = null;
},
))
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Stack(children: [
Align(
alignment: Alignment.topRight,
child: IconButton(
icon: Icon(Icons.highlight_remove),
tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = null;
},
)),
Align(
alignment: Alignment.topLeft,
child: Padding(padding: EdgeInsets.all(2.0), child: Icon(Icons.reply)),
)
]),
Wrap(
runAlignment: WrapAlignment.spaceEvenly,
alignment: WrapAlignment.center,
runSpacing: 1.0,
children: [Center(widthFactor: 1.0, child: Padding(padding: EdgeInsets.all(10.0), child: message.getPreviewWidget(context)))]),
]));
} else {
return MessageLoadingBubble();
@ -237,7 +316,7 @@ class _MessageViewState extends State<MessageView> {
children = [composeBox];
}
return Column(mainAxisSize: MainAxisSize.min, children: children);
return Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, children: children));
}
// Send the message if enter is pressed without the shift key...
@ -278,7 +357,7 @@ class _MessageViewState extends State<MessageView> {
return contact.onion != Provider.of<ContactInfoState>(context).onion;
}, onChanged: (newVal) {
setState(() {
this.selectedContact = newVal;
this.selectedContact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(newVal)!.identifier;
});
})),
SizedBox(
@ -287,7 +366,7 @@ class _MessageViewState extends State<MessageView> {
ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn),
onPressed: () {
if (this.selectedContact != "") {
if (this.selectedContact != -1) {
this._sendInvitation();
}
Navigator.pop(bcontext);
@ -298,4 +377,83 @@ class _MessageViewState extends State<MessageView> {
));
});
}
void _showFilePicker(BuildContext ctx) async {
imagePreview = null;
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
File file = File(result.files.first.path);
// We have a maximum number of bytes we can represent in terms of
// a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25)
if (file.lengthSync() <= 10737418240) {
print("Sending " + file.path);
_confirmFileSend(ctx, file.path);
} else {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.msgFileTooBig),
duration: Duration(seconds: 4),
);
ScaffoldMessenger.of(ctx).showSnackBar(snackBar);
}
}
}
void _confirmFileSend(BuildContext ctx, String path) async {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
var showPreview = false;
if (Provider.of<Settings>(context, listen: false).shouldPreview(path)) {
showPreview = true;
if (imagePreview == null) {
imagePreview = new File(path);
}
}
return Container(
height: 300, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(context)!.msgConfirmSend + " $path?"),
SizedBox(
height: 20,
),
Visibility(visible: showPreview, child: showPreview ? Image.file(
imagePreview!,
cacheHeight: 150, // limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
filterQuality: FilterQuality.medium,
fit: BoxFit.fill,
alignment: Alignment.center,
height: 150,
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
},
) : Container()),
Visibility(visible: showPreview, child: SizedBox(height: 10,)),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel, semanticsLabel: AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.pop(bcontext);
},
),
SizedBox(width: 20,),
ElevatedButton(
child: Text(AppLocalizations.of(context)!.btnSendFile, semanticsLabel: AppLocalizations.of(context)!.btnSendFile),
onPressed: () {
_sendFile(path);
Navigator.pop(bcontext);
},
),
]),
],
)),
));
});
}
}

View File

@ -69,14 +69,9 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
readonly: false,
onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var onion = Provider.of<ContactInfoState>(context, listen: false).onion;
var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
final setPeerAttribute = {
"EventType": "SetPeerAttribute",
"Data": {"RemotePeer": onion, "Key": "local.name", "Data": ctrlrNick.text},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, conversation, "profile.name", ctrlrNick.text);
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.nickChangeSuccess));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
@ -110,11 +105,15 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
height: 20,
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.blockBtn, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.blockBtn, style: TextStyle(color: settings.current().mainTextColor)),
value: Provider.of<ContactInfoState>(context).isBlocked,
onChanged: (bool blocked) {
// Save local blocked status
Provider.of<ContactInfoState>(context, listen: false).isBlocked = blocked;
if (blocked) {
Provider.of<ContactInfoState>(context, listen: false).authorization = ContactAuthorization.blocked;
} else {
Provider.of<ContactInfoState>(context, listen: false).authorization = ContactAuthorization.unknown;
}
// Save New peer authorization
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
@ -138,14 +137,14 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.block_peer, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(CwtchIcons.block_peer, color: settings.current().mainTextColor),
),
ListTile(
title: Text(AppLocalizations.of(context)!.savePeerHistory, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.savePeerHistory, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.savePeerHistoryDescription),
leading: Icon(CwtchIcons.peer_history, color: settings.current().mainTextColor()),
leading: Icon(CwtchIcons.peer_history, color: settings.current().mainTextColor),
trailing: DropdownButton(
value: Provider.of<ContactInfoState>(context).savePeerHistory == "DefaultDeleteHistory"
? AppLocalizations.of(context)!.dontSavePeerHistory
@ -192,13 +191,21 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
),
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
Tooltip(
message: AppLocalizations.of(context)!.leaveGroup,
message: AppLocalizations.of(context)!.archiveConversation,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle);
Future.delayed(Duration(milliseconds: 500), () {
Provider.of<AppState>(context, listen: false).selectedConversation = null;
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
});
},
icon: Icon(CwtchIcons.leave_chat),
label: Text(AppLocalizations.of(context)!.leaveGroup),
label: Text(AppLocalizations.of(context)!.archiveConversation),
))
])
]),
@ -216,20 +223,23 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text("Cancel"),
child: Text(AppLocalizations.of(context)!.cancel),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
Widget continueButton = ElevatedButton(
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.LeaveConversation(profileOnion, handle);
var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle);
Future.delayed(Duration(milliseconds: 500), () {
Provider.of<AppState>(context, listen: false).selectedConversation = null;
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
});
},

View File

@ -17,6 +17,7 @@ import '../model.dart';
import '../torstatus.dart';
import 'addeditprofileview.dart';
import 'globalsettingsview.dart';
import 'serversview.dart';
class ProfileMgrView extends StatefulWidget {
ProfileMgrView();
@ -39,34 +40,31 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
return Consumer<Settings>(
// Prevents Android back button from closing the app on the profile manager screen
// (which would shutdown connections and all kinds of other expensive to generate things)
// TODO pop up a dialogue regarding closing the app?
builder: (context, settings, child) => WillPopScope(
onWillPop: () async {
_modalShutdown();
return Provider.of<AppState>(context, listen: false).cwtchIsClosing;
},
child: Scaffold(
backgroundColor: settings.theme.backgroundMainColor(),
backgroundColor: settings.theme.backgroundMainColor,
appBar: AppBar(
title: Row(children: [
Image(
image: AssetImage("assets/core/knott-white.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
width: 32,
height: 32,
colorBlendMode: BlendMode.dstIn,
color: Provider.of<Settings>(context).theme.backgroundHilightElementColor(),
Icon(
CwtchIcons.cwtch_knott,
size: 36,
color: settings.theme.mainTextColor,
),
SizedBox(
width: 10,
),
Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor())))
Expanded(
child: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort,
style: TextStyle(color: settings.current().mainTextColor)))
]),
actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddEditProfile,
onPressed: _pushAddProfile,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
@ -95,10 +93,16 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
// Unlock Profiles
actions.add(IconButton(
icon: Icon(CwtchIcons.lock_open_24px),
color: Provider.of<ProfileListState>(context).profiles.isEmpty ? Provider.of<Settings>(context).theme.defaultButtonColor : Provider.of<Settings>(context).theme.mainTextColor,
tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
onPressed: _modalUnlockProfiles,
));
// Servers
if (Provider.of<Settings>(context).isExperimentEnabled(ServerManagementExperiment) && !Platform.isAndroid && !Platform.isIOS) {
actions.add(IconButton(icon: Icon(CwtchIcons.dns_black_24dp), tooltip: AppLocalizations.of(context)!.serversManagerTitleShort, onPressed: _pushServers));
}
// Global Settings
actions.add(IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings));
@ -123,6 +127,18 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
));
}
void _pushServers() {
Navigator.of(context).push(MaterialPageRoute<void>(
settings: RouteSettings(name: "servers"),
builder: (BuildContext context) {
return MultiProvider(
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
child: ServersView(),
);
},
));
}
void _pushTorStatus() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
@ -134,7 +150,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
));
}
void _pushAddEditProfile({onion: ""}) {
void _pushAddProfile({onion: ""}) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
@ -220,9 +236,9 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
).toList();
if (tiles.isEmpty) {
return const Center(
child: const Text(
"Please create or unlock a profile to begin!",
return Center(
child: Text(
AppLocalizations.of(context)!.unlockProfileTip,
textAlign: TextAlign.center,
));
}

View File

@ -0,0 +1,136 @@
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/remoteserverrow.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../cwtch_icons_icons.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
class ProfileServersView extends StatefulWidget {
@override
_ProfileServersView createState() => _ProfileServersView();
}
class _ProfileServersView extends State<ProfileServersView> {
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
var knownServers = Provider.of<ProfileInfoState>(context).serverList.servers.map<String>((RemoteServerInfoState remoteServer) {
return remoteServer.onion + ".onion";
}).toSet();
var importServerList = Provider.of<ServerListState>(context).servers.where((server) => !knownServers.contains(server.onion)).map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
return DropdownMenuItem<String>(
value: serverInfo.onion,
child: Text(
serverInfo.description.isNotEmpty ? serverInfo.description : serverInfo.onion,
overflow: TextOverflow.ellipsis,
),
);
}).toList();
importServerList.insert(0, DropdownMenuItem<String>(value: "", child: Text(AppLocalizations.of(context)!.importLocalServerSelectText)));
return Scaffold(
appBar: AppBar(
title: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.manageKnownServersLong : AppLocalizations.of(context)!.manageKnownServersShort),
),
body: Consumer<ProfileInfoState>(
builder: (context, profile, child) {
ProfileServerListState servers = profile.serverList;
final tiles = servers.servers.map(
(RemoteServerInfoState server) {
return ChangeNotifierProvider<RemoteServerInfoState>.value(
value: server,
builder: (context, child) => RepaintBoundary(child: RemoteServerRow()),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
final importCard = Card(
child: ListTile(
title: Text(AppLocalizations.of(context)!.importLocalServerLabel),
leading: Icon(CwtchIcons.add_circle_24px, color: Provider.of<Settings>(context).current().mainTextColor),
trailing: DropdownButton(
onChanged: (String? importServer) {
if (importServer!.isNotEmpty) {
var server = Provider.of<ServerListState>(context).getServer(importServer)!;
showImportConfirm(context, profile.onion, server.onion, server.description, server.serverBundle);
}
},
value: "",
items: importServerList,
)));
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
return Scrollbar(
isAlwaysShown: true,
child: SingleChildScrollView(
clipBehavior: Clip.antiAlias,
child: Container(
margin: EdgeInsets.fromLTRB(5, 0, 5, 10),
padding: EdgeInsets.fromLTRB(5, 0, 5, 10),
child: Column(children: [if (importServerList.length > 1) importCard, Column(children: divided)]))));
});
return ListView(children: divided);
},
));
}
showImportConfirm(BuildContext context, String profileHandle, String serverHandle, String serverDesc, String bundle) {
var serverLabel = serverDesc.isNotEmpty ? serverDesc : serverHandle;
serverHandle = serverHandle.substring(0, serverHandle.length - 6); // remove '.onion'
// set up the buttons
Widget cancelButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.importLocalServerButton.replaceAll("%1", serverLabel)),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileHandle, bundle);
// Wait 500ms and hope the server is imported and add it's description in the UI and as an attribute
Future.delayed(const Duration(milliseconds: 500), () {
var profile = Provider.of<ProfileInfoState>(context);
if (profile.serverList.getServer(serverHandle) != null) {
profile.serverList.getServer(serverHandle)?.updateDescription(serverDesc);
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profile.onion, profile.serverList.getServer(serverHandle)!.identifier, "server.description", serverDesc);
}
});
Navigator.of(context).pop();
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.importLocalServerButton.replaceAll("%1", serverLabel)),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}

View File

@ -0,0 +1,146 @@
import 'dart:convert';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/buttontextfield.dart';
import 'package:cwtch/widgets/contactrow.dart';
import 'package:cwtch/widgets/cwtchlabel.dart';
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:cwtch/widgets/textfield.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../errorHandler.dart';
import '../main.dart';
import '../config.dart';
import '../model.dart';
/// Pane to add or edit a server
class RemoteServerView extends StatefulWidget {
const RemoteServerView();
@override
_RemoteServerViewState createState() => _RemoteServerViewState();
}
class _RemoteServerViewState extends State<RemoteServerView> {
final _formKey = GlobalKey<FormState>();
final ctrlrDesc = TextEditingController(text: "");
@override
void initState() {
super.initState();
var serverInfoState = Provider.of<RemoteServerInfoState>(context, listen: false);
if (serverInfoState.description.isNotEmpty) {
ctrlrDesc.text = serverInfoState.description;
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer3<ProfileInfoState, RemoteServerInfoState, Settings>(builder: (context, profile, serverInfoState, settings, child) {
return Scaffold(
appBar: AppBar(title: Text(ctrlrDesc.text.isNotEmpty ? ctrlrDesc.text : serverInfoState.onion)),
body: Container(
margin: EdgeInsets.fromLTRB(30, 0, 30, 10),
padding: EdgeInsets.fromLTRB(20, 0, 20, 10),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress),
SizedBox(
height: 20,
),
SelectableText(serverInfoState.onion),
// Description
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
Text(AppLocalizations.of(context)!.serverDescriptionDescription),
SizedBox(
height: 20,
),
CwtchButtonTextField(
controller: ctrlrDesc,
readonly: false,
tooltip: AppLocalizations.of(context)!.saveBtn,
labelText: AppLocalizations.of(context)!.fieldDescriptionLabel,
icon: Icon(Icons.save),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profile.onion, serverInfoState.identifier, "server.description", ctrlrDesc.text);
serverInfoState.updateDescription(ctrlrDesc.text);
},
),
SizedBox(
height: 20,
),
Padding(
padding: EdgeInsets.all(8),
child: Text(AppLocalizations.of(context)!.groupsOnThisServerLabel),
),
Expanded(child: _buildGroupsList(serverInfoState))
])));
});
}
Widget _buildGroupsList(RemoteServerInfoState serverInfoState) {
final tiles = serverInfoState.groups.map(
(ContactInfoState group) {
return ChangeNotifierProvider<ContactInfoState>.value(
value: group,
builder: (context, child) => RepaintBoundary(child: _buildGroupRow(group)), // ServerRow()),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
var size = MediaQuery.of(context).size;
int cols = ((size.width - 50) / 500).ceil();
final double itemHeight = 60; // magic arbitary
final double itemWidth = (size.width - 50 /* magic padding guess */) / cols;
return GridView.count(crossAxisCount: cols, childAspectRatio: (itemWidth / itemHeight), children: divided);
}
Widget _buildGroupRow(ContactInfoState group) {
return Padding(
padding: const EdgeInsets.all(6.0), //border size
child: Column(children: [
Text(
group.nickname,
style: Provider.of<FlwtchState>(context).biggerFont.apply(color: Provider.of<Settings>(context).theme.portraitOnlineBorderColor),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: ExcludeSemantics(
child: Text(
group.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Provider.of<Settings>(context).theme.portraitOnlineBorderColor),
)))
]));
}
}

156
lib/views/serversview.dart Normal file
View File

@ -0,0 +1,156 @@
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/views/addeditservers.dart';
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:cwtch/widgets/serverrow.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/torstatus.dart';
import 'package:cwtch/widgets/tor_icon.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../cwtch_icons_icons.dart';
import '../main.dart';
import '../settings.dart';
///
class ServersView extends StatefulWidget {
@override
_ServersView createState() => _ServersView();
}
class _ServersView extends State<ServersView> {
final ctrlrPassword = TextEditingController();
@override
void dispose() {
ctrlrPassword.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Provider.of<Settings>(context, listen: false).theme.backgroundMainColor,
appBar: AppBar(
title: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort),
actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddServer,
tooltip: AppLocalizations.of(context)!.addServerTooltip,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addServerTooltip,
),
),
body: Consumer<ServerListState>(
builder: (context, svrs, child) {
final tiles = svrs.servers.map(
(ServerInfoState server) {
return ChangeNotifierProvider<ServerInfoState>.value(
value: server,
builder: (context, child) => RepaintBoundary(child: ServerRow()),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
if (tiles.isEmpty) {
return Center(
child: Text(
AppLocalizations.of(context)!.unlockServerTip,
textAlign: TextAlign.center,
));
}
return ListView(children: divided);
},
));
}
List<Widget> getActions() {
List<Widget> actions = new List<Widget>.empty(growable: true);
// Unlock Profiles
actions.add(IconButton(
icon: Icon(CwtchIcons.lock_open_24px),
color: Provider.of<ServerListState>(context).servers.isEmpty ? Provider.of<Settings>(context).theme.defaultButtonColor : Provider.of<Settings>(context).theme.mainTextColor,
tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
onPressed: _modalUnlockServers,
));
return actions;
}
void _modalUnlockServers() {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(context)!.enterServerPassword),
SizedBox(
height: 20,
),
CwtchPasswordField(
autofocus: true,
controller: ctrlrPassword,
action: unlock,
validator: (value) {},
),
SizedBox(
height: 20,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock),
onPressed: () {
unlock(ctrlrPassword.value.text);
},
)),
Spacer()
]),
],
))),
)));
});
}
void unlock(String password) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LoadServers(password);
ctrlrPassword.text = "";
Navigator.pop(context);
}
void _pushAddServer() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ServerInfoState>(
create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true),
)
],
child: AddEditServerView(),
);
},
));
}
}

View File

@ -1,11 +1,17 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../model.dart';
import '../settings.dart';
class SplashView extends StatelessWidget {
class SplashView extends StatefulWidget {
@override
_SplashViewState createState() => _SplashViewState();
}
class _SplashViewState extends State<SplashView> {
@override
Widget build(BuildContext context) {
return Consumer<AppState>(
@ -25,11 +31,24 @@ class SplashView extends StatelessWidget {
isAntiAlias: true,
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Text(appState.appError == "" ? "Loading Cwtch..." : appState.appError,
style: TextStyle(
fontSize: 16.0, color: appState.appError == "" ? Provider.of<Settings>(context).theme.mainTextColor() : Provider.of<Settings>(context).theme.textfieldErrorColor())),
),
padding: const EdgeInsets.all(20.0),
child: Column(children: [
Padding(
padding: EdgeInsets.all(6.0),
child: Text(
appState.appError != ""
? appState.appError
: appState.modalState == ModalState.none
? AppLocalizations.of(context)!.loadingCwtch
: AppLocalizations.of(context)!.storageMigrationModalMessage,
style: TextStyle(
fontSize: 16.0, color: appState.appError == "" ? Provider.of<Settings>(context).theme.mainTextColor : Provider.of<Settings>(context).theme.textfieldErrorColor))),
Visibility(
visible: appState.modalState == ModalState.storageMigration,
child: LinearProgressIndicator(
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
))
])),
Image(image: AssetImage("assets/Open_Privacy_Logo_lightoutline.png")),
])),
));

View File

@ -54,7 +54,7 @@ class _TorStatusView extends State<TorStatusView> {
),
ListTile(
title: Text(AppLocalizations.of(context)!.torVersion),
subtitle: Text(torStatus.version),
subtitle: SelectableText(torStatus.version),
),
]))));
});

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'package:cwtch/views/profilemgrview.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
import 'contactsview.dart';
import 'messageview.dart';
// currently unused but maybe one day?
class TripleColumnView extends StatefulWidget {
@override
_TripleColumnViewState createState() => _TripleColumnViewState();
}
class _TripleColumnViewState extends State<TripleColumnView> {
@override
Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context);
var settings = Provider.of<Settings>(context);
var columns = settings.uiColumns(appState.isLandscape(context));
return Flex(direction: Axis.horizontal, children: <Widget>[
Flexible(
flex: columns[0],
child: ProfileMgrView(),
),
Flexible(
flex: columns[1],
child: appState.selectedProfile == null ? Center(child: Text(AppLocalizations.of(context)!.createProfileToBegin)) : ContactsView(), //dev
),
Flexible(
flex: columns[2],
child: appState.selectedConversation == null
? Center(child: Text(AppLocalizations.of(context)!.addContactFirst))
: //dev
Container(child: MessageView()),
),
]);
}
}

View File

@ -5,12 +5,13 @@ import 'package:provider/provider.dart';
// Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator.
class CwtchButtonTextField extends StatefulWidget {
CwtchButtonTextField({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true});
CwtchButtonTextField({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true, this.labelText});
final TextEditingController controller;
final Function()? onPressed;
final Icon icon;
final String tooltip;
final bool readonly;
String? labelText;
@override
_CwtchButtonTextFieldState createState() => _CwtchButtonTextFieldState();
@ -37,30 +38,33 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
readOnly: widget.readonly,
showCursor: !widget.readonly,
focusNode: _focusNode,
enableIMEPersonalizedLearning: false,
decoration: InputDecoration(
labelText: widget.labelText,
labelStyle: TextStyle(color: theme.current().mainTextColor, backgroundColor: theme.current().textfieldBackgroundColor),
suffixIcon: IconButton(
onPressed: widget.onPressed,
icon: widget.icon,
padding: EdgeInsets.fromLTRB(0.0, 4.0, 2.0, 2.0),
tooltip: widget.tooltip,
enableFeedback: true,
color: theme.current().mainTextColor(),
highlightColor: theme.current().defaultButtonColor(),
focusColor: theme.current().defaultButtonActiveColor(),
splashColor: theme.current().defaultButtonActiveColor(),
color: theme.current().mainTextColor,
highlightColor: theme.current().defaultButtonColor,
focusColor: theme.current().defaultButtonActiveColor,
splashColor: theme.current().defaultButtonActiveColor,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
filled: true,
fillColor: theme.current().textfieldBackgroundColor(),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
fillColor: theme.current().textfieldBackgroundColor,
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)),
errorStyle: TextStyle(
color: theme.current().textfieldErrorColor(),
color: theme.current().textfieldErrorColor,
fontWeight: FontWeight.bold,
),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0))),
);
});
}

View File

@ -1,3 +1,6 @@
import 'dart:io';
import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:cwtch/views/messageview.dart';
@ -20,24 +23,25 @@ class _ContactRowState extends State<ContactRow> {
var contact = Provider.of<ContactInfoState>(context);
return Card(
clipBehavior: Clip.antiAlias,
color: Provider.of<AppState>(context).selectedConversation == contact.onion ? Provider.of<Settings>(context).theme.backgroundHilightElementColor() : null,
color: Provider.of<AppState>(context).selectedConversation == contact.identifier ? Provider.of<Settings>(context).theme.backgroundHilightElementColor : null,
borderOnForeground: false,
margin: EdgeInsets.all(0.0),
child: InkWell(
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Padding(
padding: const EdgeInsets.all(2.0), //border size
padding: const EdgeInsets.all(6.0), //border size
child: ProfileImage(
badgeCount: contact.unreadMessages,
badgeColor: Provider.of<Settings>(context).theme.portraitContactBadgeColor(),
badgeTextColor: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor(),
badgeColor: Provider.of<Settings>(context).theme.portraitContactBadgeColor,
badgeTextColor: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
diameter: 64.0,
imagePath: contact.imagePath,
maskOut: !contact.isOnline(),
border: contact.isOnline()
? Provider.of<Settings>(context).theme.portraitOnlineBorderColor()
? Provider.of<Settings>(context).theme.portraitOnlineBorderColor
: contact.isBlocked
? Provider.of<Settings>(context).theme.portraitBlockedBorderColor()
: Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
? Provider.of<Settings>(context).theme.portraitBlockedBorderColor
: Provider.of<Settings>(context).theme.portraitOfflineBorderColor),
),
Expanded(
child: Padding(
@ -51,13 +55,21 @@ class _ContactRowState extends State<ContactRow> {
style: TextStyle(
fontSize: Provider.of<Settings>(context).theme.contactOnionTextSize(),
color: contact.isBlocked
? Provider.of<Settings>(context).theme.portraitBlockedTextColor()
: Provider.of<Settings>(context).theme.mainTextColor()), //Provider.of<FlwtchState>(context).biggerFont,
? Provider.of<Settings>(context).theme.portraitBlockedTextColor
: Provider.of<Settings>(context).theme.mainTextColor), //Provider.of<FlwtchState>(context).biggerFont,
softWrap: true,
overflow: TextOverflow.visible,
),
Text(contact.onion,
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor() : Provider.of<Settings>(context).theme.mainTextColor())),
Visibility(
visible: contact.isGroup && contact.status == "Authenticated",
child: LinearProgressIndicator(
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
)),
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: Text(contact.onion,
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor : Provider.of<Settings>(context).theme.mainTextColor)),
)
],
))),
Padding(
@ -69,7 +81,7 @@ class _ContactRowState extends State<ContactRow> {
iconSize: 16,
icon: Icon(
Icons.favorite,
color: Provider.of<Settings>(context).theme.mainTextColor(),
color: Provider.of<Settings>(context).theme.mainTextColor,
),
tooltip: AppLocalizations.of(context)!.tooltipAcceptContactRequest,
onPressed: _btnApprove,
@ -77,7 +89,7 @@ class _ContactRowState extends State<ContactRow> {
IconButton(
padding: EdgeInsets.zero,
iconSize: 16,
icon: Icon(Icons.delete, color: Provider.of<Settings>(context).theme.mainTextColor()),
icon: Icon(Icons.delete, color: Provider.of<Settings>(context).theme.mainTextColor),
tooltip: AppLocalizations.of(context)!.tooltipRejectContactRequest,
onPressed: _btnReject,
)
@ -86,60 +98,33 @@ class _ContactRowState extends State<ContactRow> {
? IconButton(
padding: EdgeInsets.zero,
iconSize: 16,
icon: Icon(Icons.block, color: Provider.of<Settings>(context).theme.mainTextColor()),
icon: Icon(Icons.block, color: Provider.of<Settings>(context).theme.mainTextColor),
onPressed: () {},
)
: Text(dateToNiceString(contact.lastMessageTime))),
),
]),
onTap: () {
setState(() {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contact.onion)!.unreadMessages = 0;
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).selectedConversation = contact.onion;
Provider.of<AppState>(context, listen: false).selectedIndex = null;
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(contact.onion);
});
selectConversation(context, contact.identifier);
},
));
}
void _pushMessageView(String handle) {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
// assert we have an actual profile...
// We need to listen for updates to the profile in order to update things like invitation message bubbles.
var profile = Provider.of<FlwtchState>(builderContext).profs.getProfile(profileOnion)!;
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
],
builder: (context, child) => MessageView(),
);
},
),
);
}
void _btnApprove() {
// Update the UI
Provider.of<ContactInfoState>(context, listen: false).authorization = ContactAuthorization.approved;
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.AcceptContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion);
.AcceptContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier);
}
void _btnReject() {
ContactInfoState contact = Provider.of<ContactInfoState>(context, listen: false);
if (contact.isGroup == true) {
Provider.of<FlwtchState>(context, listen: false).cwtch.RejectInvite(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.onion);
// FIXME This flow is incrorect. Groups never just show up on the contact list anymore
Provider.of<ProfileInfoState>(context, listen: false).removeContact(contact.onion);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.BlockContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.onion);
Provider.of<FlwtchState>(context, listen: false).cwtch.BlockContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.identifier);
}
}
@ -149,9 +134,9 @@ class _ContactRowState extends State<ContactRow> {
}
// If the last message was over a day ago, just state the date
if (DateTime.now().difference(date).inDays > 1) {
return DateFormat.yMd().format(date.toLocal());
return DateFormat.yMd(Platform.localeName).format(date.toLocal());
}
// Otherwise just state the time.
return DateFormat.Hm().format(date.toLocal());
return DateFormat.Hm(Platform.localeName).format(date.toLocal());
}
}

View File

@ -18,7 +18,7 @@ class _CwtchLabelState extends State<CwtchLabel> {
return Consumer<Settings>(builder: (context, theme, child) {
return Text(
widget.label,
style: TextStyle(fontSize: 20, color: theme.current().mainTextColor()),
style: TextStyle(fontSize: 20, color: theme.current().mainTextColor),
);
});
}

386
lib/widgets/filebubble.dart Normal file
View File

@ -0,0 +1,386 @@
import 'dart:io';
import 'package:cwtch/config.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:file_picker_desktop/file_picker_desktop.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../model.dart';
import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../settings.dart';
import 'messagebubbledecorations.dart';
// Like MessageBubble but for displaying chat overlay 100/101 invitations
// Offers the user an accept/reject button if they don't have a matching contact already
class FileBubble extends StatefulWidget {
final String nameSuggestion;
final String rootHash;
final String nonce;
final int fileSize;
final bool interactive;
final bool isAuto;
FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true});
@override
FileBubbleState createState() => FileBubbleState();
String fileKey() {
return this.rootHash + "." + this.nonce;
}
}
class FileBubbleState extends State<FileBubble> {
File? myFile;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context, listen: false).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var flagStarted = Provider.of<MessageMetadata>(context).attributes["file-downloaded"] == "true";
var borderRadiousEh = 15.0;
var showFileSharing = Provider.of<Settings>(context, listen: false).isExperimentEnabled(FileSharingExperiment);
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
var downloadComplete = Provider.of<ProfileInfoState>(context).downloadComplete(widget.fileKey());
var downloadInterrupted = Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey());
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey());
if (downloadComplete) {
var lpath = path!.toLowerCase();
if (lpath.endsWith(".jpg") || lpath.endsWith(".jpeg") || lpath.endsWith(".png") || lpath.endsWith(".gif") || lpath.endsWith(".webp") || lpath.endsWith(".bmp")) {
if (myFile == null) {
setState(() {
myFile = new File(path);
});
}
}
}
var downloadActive = Provider.of<ProfileInfoState>(context).downloadActive(widget.fileKey());
var downloadGotManifest = Provider.of<ProfileInfoState>(context).downloadGotManifest(widget.fileKey());
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
var senderIsContact = false;
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
senderIsContact = true;
} else {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
return LayoutBuilder(builder: (bcontext, constraints) {
var wdgSender = Visibility(
visible: widget.interactive,
child: SelectableText(senderDisplayStr + '\u202F',
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor)));
var isPreview = false;
var wdgMessage = !showFileSharing
? Text(AppLocalizations.of(context)!.messageEnableFileSharing)
: fromMe
? senderFileChrome(AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize)
: (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize,
Provider.of<ProfileInfoState>(context).downloadSpeed(widget.fileKey())));
Widget wdgDecorations;
if (!showFileSharing) {
wdgDecorations = Text('\u202F');
} else if (fromMe) {
wdgDecorations = Visibility(
visible: widget.interactive,
child: MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate));
} else if (downloadComplete) {
// in this case, whatever marked download.complete would have also set the path
if (Provider.of<Settings>(context).shouldPreview(path!)) {
isPreview = true;
wdgDecorations = Center(
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: Padding(
padding: EdgeInsets.all(1.0),
child: Image.file(
myFile!,
cacheWidth: 2048,
// limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
filterQuality: FilterQuality.medium,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
height: MediaQuery.of(bcontext).size.height * 0.30,
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
},
)),
onTap: () {
pop(bcontext, myFile!, wdgMessage);
},
)));
} else {
wdgDecorations = Visibility(visible: widget.interactive, child: Text(AppLocalizations.of(context)!.fileSavedTo + ': ' + path + '\u202F'));
}
} else if (downloadActive) {
if (!downloadGotManifest) {
wdgDecorations = Visibility(visible: widget.interactive, child: Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'));
} else {
wdgDecorations = Visibility(
visible: widget.interactive,
child: LinearProgressIndicator(
value: Provider.of<ProfileInfoState>(context).downloadProgress(widget.fileKey()),
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
));
}
} else if (flagStarted) {
// in this case, the download was done in a previous application launch,
// so we probably have to request an info lookup
if (!downloadInterrupted) {
wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F');
} else {
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()) ?? "";
wdgDecorations = Visibility(
visible: widget.interactive,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),
ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton))
]));
}
} else if (!senderIsContact) {
wdgDecorations = Text(AppLocalizations.of(context)!.msgAddToAccept);
} else if (!widget.isAuto) {
wdgDecorations = Visibility(
visible: widget.interactive,
child: Center(
widthFactor: 1,
child: Wrap(children: [
Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.downloadFileButton + '\u202F'), onPressed: _btnAccept)),
])));
} else {
wdgDecorations = Container();
}
return Container(
constraints: constraints,
decoration: BoxDecoration(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor,
border: Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor, width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
),
),
child: Padding(
padding: EdgeInsets.all(9.0),
child: Column(
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)] : [wdgSender, isPreview ? Container() : wdgMessage, wdgDecorations]),
));
});
}
void _btnAccept() async {
String? selectedFileName;
File? file;
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageID;
if (Platform.isAndroid) {
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
//Provider.of<MessageMetadata>(context, listen: false).attributes |= 0x02;
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey());
}
} else {
try {
selectedFileName = await saveFile(
defaultFileName: widget.nameSuggestion,
);
if (selectedFileName != null) {
file = File(selectedFileName);
EnvironmentConfig.debugLog("saving to " + file.path);
var manifestPath = file.path + ".manifest";
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
//Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
ContactInfoState? contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey());
}
}
} catch (e) {
print(e);
}
}
}
void _btnResume() async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var handle = Provider.of<MessageMetadata>(context, listen: false).conversationIdentifier;
Provider.of<ProfileInfoState>(context, listen: false).downloadMarkResumed(widget.fileKey());
Provider.of<FlwtchState>(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey());
}
// Construct an file chrome for the sender
Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) {
return ListTile(
visualDensity: VisualDensity.compact,
title: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.start, children: [
SelectableText(
chrome + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,
),
SelectableText(
fileName + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.parent,
maxLines: 2,
),
SelectableText(
prettyBytes(fileSize) + '\u202F' + '\n',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
),
textAlign: TextAlign.left,
maxLines: 2,
)
]),
subtitle: SelectableText(
'sha512: ' + rootHash + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
fontSize: 10,
fontFamily: "monospace",
),
textAlign: TextAlign.left,
maxLines: 4,
textWidthBasis: TextWidthBasis.parent,
),
leading: Icon(Icons.attach_file, size: 32, color: Provider.of<Settings>(context).theme.messageFromMeTextColor));
}
// Construct an file chrome
Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) {
return ListTile(
visualDensity: VisualDensity.compact,
title: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.start, children: [
SelectableText(
chrome + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,
),
SelectableText(
fileName + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.parent,
maxLines: 2,
),
SelectableText(
AppLocalizations.of(context)!.labelFilesize + ': ' + prettyBytes(fileSize) + '\u202F' + '\n',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
textAlign: TextAlign.left,
maxLines: 2,
)
]),
subtitle: SelectableText(
'sha512: ' + rootHash + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
fontSize: 10,
fontFamily: "monospace",
),
textAlign: TextAlign.left,
maxLines: 4,
textWidthBasis: TextWidthBasis.parent,
),
leading: Icon(Icons.attach_file, size: 32, color: Provider.of<Settings>(context).theme.messageFromOtherTextColor),
trailing: Visibility(
visible: speed != "0 B/s",
child: SelectableText(
speed + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
),
textAlign: TextAlign.left,
maxLines: 1,
textWidthBasis: TextWidthBasis.longestLine,
)),
);
}
void pop(context, File myFile, Widget meta) async {
await showDialog(
context: context,
builder: (_) => Dialog(
alignment: Alignment.center,
child: Container(
padding: EdgeInsets.all(10),
child: Column(children: [
ListTile(
title: meta,
trailing: IconButton(
icon: Icon(Icons.close),
color: Provider.of<Settings>(context, listen: false).theme.toolbarIconColor,
iconSize: 32,
onPressed: () {
Navigator.pop(context, true);
})),
Image.file(
myFile,
cacheWidth: (MediaQuery.of(context).size.width * 0.6).floor(),
width: (MediaQuery.of(context).size.width * 0.6),
height: (MediaQuery.of(context).size.height * 0.6),
fit: BoxFit.scaleDown,
),
SizedBox(
height: 20,
),
Visibility(visible: !Platform.isAndroid, child: Text(myFile.path, textAlign: TextAlign.center)),
Visibility(visible: Platform.isAndroid, child: IconButton(icon: Icon(Icons.arrow_downward), onPressed: androidExport)),
]),
)));
}
void androidExport() async {
if (myFile != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.ExportPreviewedFile(myFile!.path, widget.nameSuggestion);
}
}
}

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:io';
import 'package:file_picker_desktop/file_picker_desktop.dart';
import 'buttontextfield.dart';
import 'cwtchlabel.dart';
class CwtchFolderPicker extends StatefulWidget {
final String label;
final String initialValue;
final Function(String)? onSave;
const CwtchFolderPicker({Key? key, this.label = "", this.initialValue = "", this.onSave}) : super(key: key);
@override
_CwtchFolderPickerState createState() => _CwtchFolderPickerState();
}
class _CwtchFolderPickerState extends State<CwtchFolderPicker> {
final TextEditingController ctrlrVal = TextEditingController();
@override
void initState() {
super.initState();
ctrlrVal.text = widget.initialValue;
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(2),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: widget.label),
SizedBox(
height: 20,
),
CwtchButtonTextField(
controller: ctrlrVal,
readonly: Platform.isAndroid,
onPressed: () async {
if (Platform.isAndroid) {
return;
}
try {
var selectedDirectory = await getDirectoryPath();
if (selectedDirectory != null) {
//File directory = File(selectedDirectory);
selectedDirectory += "/";
ctrlrVal.text = selectedDirectory;
if (widget.onSave != null) {
widget.onSave!(selectedDirectory);
}
} else {
// User canceled the picker
}
} catch (e) {
print(e);
}
},
icon: Icon(Icons.folder),
tooltip: "Browse", //todo: l18n
)
]));
}
}

View File

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart';
@ -19,8 +20,9 @@ class InvitationBubble extends StatefulWidget {
final int overlay;
final String inviteTarget;
final String inviteNick;
final String invite;
InvitationBubble(this.overlay, this.inviteTarget, this.inviteNick);
InvitationBubble(this.overlay, this.inviteTarget, this.inviteNick, this.invite);
@override
InvitationBubbleState createState() => InvitationBubbleState();
@ -34,16 +36,16 @@ class InvitationBubbleState extends State<InvitationBubble> {
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var isGroup = widget.overlay == InviteGroupOverlay;
isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget) != null;
isAccepted = Provider.of<ProfileInfoState>(context).contactList.findContact(widget.inviteTarget) != null;
var borderRadiousEh = 15.0;
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
rejected = Provider.of<MessageMetadata>(context).flags & 0x01 == 0x01;
var prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
rejected = Provider.of<MessageMetadata>(context).attributes["rejected-invite"] == "true";
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
@ -54,7 +56,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
var wdgSender = Center(
widthFactor: 1,
child: SelectableText(senderDisplayStr + '\u202F',
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor())));
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor)));
// If we receive an invite for ourselves, treat it as a bug. The UI no longer allows this so it could have only come from
// some kind of malfeasance.
@ -67,7 +69,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning)
: fromMe
? senderInviteChrome(
AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget)!.nickname : widget.inviteTarget)
AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of<ProfileInfoState>(context).contactList.findContact(widget.inviteTarget)!.nickname : widget.inviteTarget)
: (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget));
Widget wdgDecorations;
@ -83,8 +85,8 @@ class InvitationBubbleState extends State<InvitationBubble> {
wdgDecorations = Center(
widthFactor: 1,
child: Wrap(children: [
Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text(AppLocalizations.of(context)!.rejectGroupBtn + '\u202F'), onPressed: _btnReject)),
Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text(AppLocalizations.of(context)!.acceptGroupBtn + '\u202F'), onPressed: _btnAccept)),
Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.rejectGroupBtn + '\u202F'), onPressed: _btnReject)),
Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.acceptGroupBtn + '\u202F'), onPressed: _btnAccept)),
]));
}
@ -94,9 +96,8 @@ class InvitationBubbleState extends State<InvitationBubble> {
widthFactor: 1.0,
child: Container(
decoration: BoxDecoration(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(),
border:
Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(), width: 1),
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor,
border: Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor, width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
@ -126,17 +127,17 @@ class InvitationBubbleState extends State<InvitationBubble> {
void _btnReject() {
setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x01);
Provider.of<MessageMetadata>(context).flags |= 0x01;
var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageID;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "rejected-invite", "true");
//Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01;
});
}
void _btnAccept() {
setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, widget.inviteTarget);
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, widget.invite);
isAccepted = true;
});
}
@ -147,7 +148,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
SelectableText(
chrome + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
),
textAlign: TextAlign.left,
maxLines: 2,
@ -156,7 +157,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
SelectableText(
targetName + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
),
textAlign: TextAlign.left,
maxLines: 2,
@ -171,7 +172,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
SelectableText(
chrome + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
@ -179,7 +180,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
),
SelectableText(
targetName + '\u202F',
style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,

View File

@ -1,9 +1,15 @@
import 'dart:io';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import 'package:intl/intl.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart';
import 'messagebubbledecorations.dart';
@ -26,14 +32,15 @@ class MessageBubbleState extends State<MessageBubble> {
var prettyDate = "";
var borderRadiousEh = 15.0;
// var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal());
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
@ -41,18 +48,42 @@ class MessageBubbleState extends State<MessageBubble> {
}
}
var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor));
var wdgMessage = SelectableText(
widget.content + '\u202F',
//key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
var wdgMessage;
if (!showClickableLinks) {
wdgMessage = SelectableText(
widget.content + '\u202F',
//key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
} else {
wdgMessage = SelectableLinkify(
text: widget.content + '\u202F',
// TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
options: LinkifyOptions(humanize: false, removeWww: false, looseUrl: true, defaultToHttps: true),
linkifiers: [UrlLinkifier()],
onOpen: (link) {
_modalOpenLink(context, link);
},
//key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
linkStyle: TextStyle(
color: Provider.of<Settings>(context).current().mainTextColor,
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
}
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
@ -64,13 +95,11 @@ class MessageBubbleState extends State<MessageBubble> {
child: Container(
child: Container(
decoration: BoxDecoration(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
color: error ? malformedColor : (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor),
border: Border.all(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor),
width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
@ -88,4 +117,57 @@ class MessageBubbleState extends State<MessageBubble> {
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
});
}
void _modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
"Opening this link will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open links from people you trust. Are you sure you want to continue?"),
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text("Copy link", semanticsLabel: "Copy link"),
onPressed: () {
Clipboard.setData(new ClipboardData(text: link.url));
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.copiedClipboardNotification),
);
Navigator.pop(bcontext);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
),
),
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text("Open link", semanticsLabel: "Open link"),
onPressed: () async {
if (await canLaunch(link.url)) {
await launch(link.url);
Navigator.pop(bcontext);
} else {
throw 'Could not launch $link';
}
},
),
),
]),
],
)),
));
});
}
}

View File

@ -25,8 +25,7 @@ class _MessageBubbleDecoration extends State<MessageBubbleDecoration> {
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.prettyDate,
style:
TextStyle(fontSize: 9.0, color: widget.fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
style: TextStyle(fontSize: 9.0, color: widget.fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor),
textAlign: widget.fromMe ? TextAlign.right : TextAlign.left),
!widget.fromMe
? SizedBox(width: 1, height: 1)
@ -35,16 +34,15 @@ class _MessageBubbleDecoration extends State<MessageBubbleDecoration> {
child: widget.ackd == true
? Tooltip(
message: AppLocalizations.of(context)!.acknowledgedLabel,
child: Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))
child: Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor, size: 16))
: (widget.errored == true
? Tooltip(
message: AppLocalizations.of(context)!.couldNotSendMsgError,
child: Icon(Icons.error_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))
child: Icon(Icons.error_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor, size: 16))
: Tooltip(
message: AppLocalizations.of(context)!.pendingLabel,
child: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))))
child: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor, size: 16))))
],
));
;
}
}

View File

@ -1,26 +1,26 @@
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
import 'messagerow.dart';
class MessageList extends StatefulWidget {
ItemScrollController scrollController;
ItemPositionsListener scrollListener;
MessageList(this.scrollController, this.scrollListener);
@override
_MessageListState createState() => _MessageListState();
}
class _MessageListState extends State<MessageList> {
ScrollController ctrlr1 = ScrollController();
@override
Widget build(BuildContext outerContext) {
var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
bool isGroupAndSynced = Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context).status == "Synced";
@ -35,63 +35,66 @@ class _MessageListState extends State<MessageList> {
return RepaintBoundary(
child: Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor,
child: Column(children: [
Visibility(
visible: showMessageWarning,
child: Container(
padding: EdgeInsets.all(5.0),
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
child: showSyncing
? Text(AppLocalizations.of(context)!.serverNotSynced, textAlign: TextAlign.center)
: showOfflineWarning
? Text(Provider.of<ContactInfoState>(context).isGroup ? AppLocalizations.of(context)!.serverConnectivityDisconnected : AppLocalizations.of(context)!.peerOfflineMessage,
textAlign: TextAlign.center)
// Only show the ephemeral status for peer conversations, not for groups...
: (showEphemeralWarning
? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center)
:
// We are not allowed to put null here, so put an empty text widge
Text("")),
)),
Expanded(
child: Scrollbar(
controller: ctrlr1,
child: Container(
// Only show broken heart is the contact is offline...
decoration: BoxDecoration(
image: Provider.of<ContactInfoState>(outerContext).isOnline()
? null
: DecorationImage(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
image: AssetImage("assets/core/negative_heart_512px.png"),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementTextColor(), BlendMode.srcIn))),
// Don't load messages for syncing server...
child: loadMessages
? ListView.builder(
controller: ctrlr1,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
itemBuilder: (itemBuilderContext, index) {
var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).onion;
var messageIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
Visibility(
visible: showMessageWarning,
child: Container(
padding: EdgeInsets.all(5.0),
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
child: DefaultTextStyle(
style: TextStyle(color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
child: showSyncing
? Text(AppLocalizations.of(context)!.serverNotSynced, textAlign: TextAlign.center)
: showOfflineWarning
? Text(Provider.of<ContactInfoState>(context).isGroup ? AppLocalizations.of(context)!.serverConnectivityDisconnected : AppLocalizations.of(context)!.peerOfflineMessage,
textAlign: TextAlign.center)
// Only show the ephemeral status for peer conversations, not for groups...
: (showEphemeralWarning
? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center)
:
// We are not allowed to put null here, so put an empty text widget
Text("")),
))),
Expanded(
child: Container(
// Only show broken heart is the contact is offline...
decoration: BoxDecoration(
image: Provider.of<ContactInfoState>(outerContext).isOnline()
? null
: DecorationImage(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
image: AssetImage("assets/core/negative_heart_512px.png"),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor, BlendMode.srcIn))),
// Don't load messages for syncing server...
child: loadMessages
? ScrollablePositionedList.builder(
itemPositionsListener: widget.scrollListener,
itemScrollController: widget.scrollController,
initialScrollIndex: initi > 4 ? initi - 4 : 0,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
itemBuilder: (itemBuilderContext, index) {
var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).identifier;
var messageIndex = index;
return FutureBuilder(
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
builder: (context, snapshot) {
if (snapshot.hasData) {
var message = snapshot.data as Message;
// Already includes MessageRow,,
return message.getWidget(context);
} else {
return MessageLoadingBubble();
}
return FutureBuilder(
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
builder: (context, snapshot) {
if (snapshot.hasData) {
var message = snapshot.data as Message;
var key = Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, message.getMetadata().messageID);
return message.getWidget(context, key);
} else {
return MessageLoadingBubble();
}
},
);
},
);
},
)
: null)))
])));
)
: null))
])));
}
}

View File

@ -1,9 +1,4 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import 'package:intl/intl.dart';
import '../settings.dart';
class MessageLoadingBubble extends StatefulWidget {
@override

View File

@ -1,8 +1,11 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/physics.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -12,99 +15,305 @@ import '../settings.dart';
class MessageRow extends StatefulWidget {
final Widget child;
MessageRow(this.child, {Key? key}) : super(key: key);
@override
MessageRowState createState() => MessageRowState();
}
class MessageRowState extends State<MessageRow> {
bool showMenu = false;
class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMixin {
bool showBlockedMessage = false;
late AnimationController _controller;
late Animation<Alignment> _animation;
late Alignment _dragAlignment = Alignment.center;
Alignment _dragAffinity = Alignment.center;
late int index;
@override
void initState() {
super.initState();
index = Provider.of<MessageMetadata>(context, listen: false).messageID;
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
@override
void dispose() {
if (_controller != null) {
_controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var isContact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle) != null;
var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false;
var actualMessage = Flexible(flex: Platform.isAndroid ? 10 : 3, fit: FlexFit.loose, child: widget.child);
Widget wdgIcons = Visibility(
visible: this.showMenu,
child: IconButton(
tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context).messageIndex;
},
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
_dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft;
if (_dragAlignment == Alignment.center) {
_dragAlignment = fromMe ? Alignment.centerRight : Alignment.centerLeft;
}
var senderDisplayStr = "";
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
Widget wdgIcons = Platform.isAndroid
? SizedBox.shrink()
: Visibility(
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
maintainInteractivity: false,
child: IconButton(
tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
},
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor)));
Widget wdgSpacer = Flexible(flex: 1, child: SizedBox(width: Platform.isAndroid ? 20 : 60, height: 10));
var widgetRow = <Widget>[];
if (fromMe) {
widgetRow = <Widget>[
wdgSpacer,
wdgIcons,
Flexible(flex: 3, fit: FlexFit.loose, child: widget.child),
actualMessage,
];
} else if (isBlocked && !showBlockedMessage) {
Color blockedMessageBackground = Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor;
Widget wdgPortrait = Padding(padding: EdgeInsets.all(4.0), child: Icon(CwtchIcons.account_blocked));
widgetRow = <Widget>[
wdgPortrait,
Container(
padding: EdgeInsets.all(2.0),
decoration: BoxDecoration(
color: blockedMessageBackground,
border: Border.all(color: blockedMessageBackground, width: 2),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15.0),
topRight: Radius.circular(15.0),
bottomLeft: Radius.circular(15.0),
bottomRight: Radius.circular(15.0),
)),
child: Padding(
padding: EdgeInsets.all(9.0),
child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
SelectableText(
AppLocalizations.of(context)!.blockedMessageMessage,
//key: Key(myKey),
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
textAlign: TextAlign.center,
textWidthBasis: TextWidthBasis.longestLine,
),
Padding(
padding: EdgeInsets.all(1.0),
child: TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(blockedMessageBackground),
),
child: Text(
AppLocalizations.of(context)!.showMessageButton + '\u202F',
style: TextStyle(decoration: TextDecoration.underline),
),
onPressed: () {
setState(() {
this.showBlockedMessage = true;
});
})),
]))),
wdgIcons,
wdgSpacer,
];
} else {
var contact = Provider.of<ContactInfoState>(context);
Widget wdgPortrait = GestureDetector(
onTap: _btnAdd,
onTap: isContact ? _btnGoto : _btnAdd,
child: Padding(
padding: EdgeInsets.all(4.0),
child: ProfileImage(
diameter: 48.0,
imagePath: Provider.of<MessageMetadata>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
badgeTextColor: Colors.red, badgeColor: Colors.red,
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor,
badgeTextColor: Colors.red,
badgeColor: Colors.red,
tooltip: isContact ? AppLocalizations.of(context)!.contactGoto.replaceFirst("%1", senderDisplayStr) : AppLocalizations.of(context)!.addContact,
)));
widgetRow = <Widget>[
wdgPortrait,
Flexible(flex: 3, fit: FlexFit.loose, child: widget.child),
actualMessage,
wdgIcons,
wdgSpacer,
];
}
return MouseRegion(
var size = MediaQuery.of(context).size;
var mr = MouseRegion(
// For desktop...
onHover: (event) {
setState(() {
this.showMenu = true;
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
});
},
onExit: (event) {
setState(() {
this.showMenu = false;
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
});
},
child: GestureDetector(
// Swipe to quote
onHorizontalDragEnd: (details) {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
onPanUpdate: (details) {
setState(() {
_dragAlignment += Alignment(
details.delta.dx / (size.width * 0.5),
0,
);
});
},
child: Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow))));
onPanDown: (details) {
_controller.stop();
},
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
},
child: Padding(
padding: EdgeInsets.all(2),
child: Align(
widthFactor: 1,
alignment: _dragAlignment,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: widgetRow,
)))));
var mark = Provider.of<ContactInfoState>(context).newMarker;
if (mark > 0 &&
Provider.of<ContactInfoState>(context).messageCache.length > mark &&
Provider.of<ContactInfoState>(context).messageCache[mark - 1]?.metadata.messageID == Provider.of<MessageMetadata>(context).messageID) {
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
} else {
return mr;
}
}
Widget _bubbleNew() {
return Container(
decoration: BoxDecoration(
color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
border: Border.all(color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor, width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
child: Padding(padding: EdgeInsets.all(9.0), child: Text(AppLocalizations.of(context)!.newMessagesLabel)));
}
void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: _dragAffinity,
),
);
// Calculate the velocity relative to the unit interval, [0,1],
// used by the animation controller.
final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;
const spring = SpringDescription(
mass: 30,
stiffness: 1,
damping: 1,
);
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
_controller.animateWith(simulation);
}
void _btnGoto() {
var id = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle)?.identifier;
if (id == null) {
// Can't happen
} else {
selectConversation(context, id);
}
}
void _btnAdd() {
var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") {
if (sender == "") {
print("sender not yet loaded");
return;
}
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
final setPeerAttribute = {
"EventType": "AddContact",
"Data": {"ImportString": sender},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.successfullAddedContact),
duration: Duration(seconds: 2),
showAddContactConfirmAlertDialog(context, profileOnion, sender);
}
showAddContactConfirmAlertDialog(BuildContext context, String profileOnion, String senderOnion) {
// set up the buttons
Widget cancelButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.cancel),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = ElevatedButton(
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.addContact),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, senderOnion);
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.successfullAddedContact),
duration: Duration(seconds: 2),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.of(context).pop(); // dismiss dialog
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.addContactConfirm.replaceFirst("%1", senderOnion)),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}

View File

@ -37,6 +37,7 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
controller: widget.controller,
validator: widget.validator,
obscureText: obscureText,
enableIMEPersonalizedLearning: false,
autofillHints: widget.autoFillHints,
autovalidateMode: AutovalidateMode.always,
onFieldSubmitted: widget.action,
@ -52,21 +53,21 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
},
icon: Icon((obscureText ? CwtchIcons.eye_closed : CwtchIcons.eye_open), semanticLabel: label),
tooltip: label,
color: theme.current().mainTextColor(),
highlightColor: theme.current().defaultButtonColor(),
focusColor: theme.current().defaultButtonActiveColor(),
splashColor: theme.current().defaultButtonActiveColor(),
color: theme.current().mainTextColor,
highlightColor: theme.current().defaultButtonColor,
focusColor: theme.current().defaultButtonActiveColor,
splashColor: theme.current().defaultButtonActiveColor,
),
errorStyle: TextStyle(
color: theme.current().textfieldErrorColor(),
color: theme.current().textfieldErrorColor,
fontWeight: FontWeight.bold,
),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)),
filled: true,
fillColor: theme.current().textfieldBackgroundColor(),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
fillColor: theme.current().textfieldBackgroundColor,
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0)),
),
);
});

View File

@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:cwtch/opaque.dart';
import 'package:cwtch/themes/opaque.dart';
import 'package:provider/provider.dart';
import '../settings.dart';
class ProfileImage extends StatefulWidget {
ProfileImage({required this.imagePath, required this.diameter, required this.border, this.badgeCount = 0, required this.badgeColor, required this.badgeTextColor, this.maskOut = false});
ProfileImage(
{required this.imagePath, required this.diameter, required this.border, this.badgeCount = 0, required this.badgeColor, required this.badgeTextColor, this.maskOut = false, this.tooltip = ""});
final double diameter;
final String imagePath;
final Color border;
@ -13,6 +14,7 @@ class ProfileImage extends StatefulWidget {
final Color badgeColor;
final Color badgeTextColor;
final bool maskOut;
final String tooltip;
@override
_ProfileImageState createState() => _ProfileImageState();
@ -21,6 +23,21 @@ class ProfileImage extends StatefulWidget {
class _ProfileImageState extends State<ProfileImage> {
@override
Widget build(BuildContext context) {
var image = Image(
image: AssetImage("assets/" + widget.imagePath),
filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: !widget.maskOut
? Provider.of<Settings>(context).theme.mode == mode_dark
? BlendMode.softLight
: BlendMode.darken
: BlendMode.srcOut,
color: Provider.of<Settings>(context).theme.portraitBackgroundColor,
isAntiAlias: true,
width: widget.diameter,
height: widget.diameter,
);
return RepaintBoundary(
child: Stack(children: [
ClipOval(
@ -31,22 +48,7 @@ class _ProfileImageState extends State<ProfileImage> {
color: widget.border,
child: Padding(
padding: const EdgeInsets.all(2.0), //border size
child: ClipOval(
clipBehavior: Clip.antiAlias,
child: Image(
image: AssetImage("assets/" + widget.imagePath),
filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: !widget.maskOut
? Provider.of<Settings>(context).theme.identifier() == "dark"
? BlendMode.softLight
: BlendMode.darken
: BlendMode.srcOut,
color: Provider.of<Settings>(context).theme.backgroundHilightElementColor(),
isAntiAlias: true,
width: widget.diameter,
height: widget.diameter,
))))),
child: ClipOval(clipBehavior: Clip.antiAlias, child: widget.tooltip == "" ? image : Tooltip(message: widget.tooltip, child: image))))),
Visibility(
visible: widget.badgeCount > 0,
child: Positioned(

View File

@ -7,6 +7,7 @@ import 'package:cwtch/widgets/profileimage.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../errorHandler.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
@ -22,19 +23,20 @@ class _ProfileRowState extends State<ProfileRow> {
var profile = Provider.of<ProfileInfoState>(context);
return Card(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.all(0.0),
child: InkWell(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(2.0), //border size
padding: const EdgeInsets.all(6.0), //border size
child: ProfileImage(
badgeCount: 0,
badgeColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor(),
badgeTextColor: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor(),
badgeColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor,
badgeTextColor: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor,
diameter: 64.0,
imagePath: profile.imagePath,
border: profile.isOnline ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor())),
border: profile.isOnline ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor)),
Expanded(
child: Column(
children: [
@ -45,20 +47,22 @@ class _ProfileRowState extends State<ProfileRow> {
softWrap: true,
overflow: TextOverflow.ellipsis,
),
ExcludeSemantics(
child: Text(
profile.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
))
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: ExcludeSemantics(
child: Text(
profile.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
)))
],
)),
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context)!.editProfile + " " + profile.nickname,
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor),
onPressed: () {
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted);
_pushEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted);
},
)
],
@ -97,7 +101,8 @@ class _ProfileRowState extends State<ProfileRow> {
);
}
void _pushAddEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
Provider.of<ErrorHandler>(context, listen: false).reset();
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(

View File

@ -34,7 +34,7 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
@ -42,13 +42,13 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
}
}
var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor));
var wdgMessage = SelectableText(
widget.body + '\u202F',
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
@ -61,13 +61,14 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
try {
var qMessage = (snapshot.data! as Message);
// Swap the background color for quoted tweets..
var qTextColor = fromMe ? Provider.of<Settings>(context).theme.messageFromOtherTextColor : Provider.of<Settings>(context).theme.messageFromMeTextColor;
return Container(
margin: EdgeInsets.all(5),
padding: EdgeInsets.all(5),
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor() : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [
Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.reply, size: 32))),
Center(widthFactor: 1.0, child: qMessage.getPreviewWidget(context))
Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.reply, size: 32, color: qTextColor))),
Center(widthFactor: 1.0, child: DefaultTextStyle(child: qMessage.getPreviewWidget(context), style: TextStyle(color: qTextColor)))
]));
} catch (e) {
print(e);
@ -89,13 +90,11 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
child: Container(
child: Container(
decoration: BoxDecoration(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
color: error ? malformedColor : (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor),
border: Border.all(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor),
width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),

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