Compare commits

...

178 Commits

Author SHA1 Message Date
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
Dan Ballard 3ed8b94274 Merge branch 'trunk' into readme-run 2021-11-24 01:01:06 +00: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
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 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
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
103 changed files with 5229 additions and 542 deletions

View File

@ -8,7 +8,7 @@ clone:
steps:
- name: clone
image: cirrusci/flutter:dev
image: cirrusci/flutter:2.5.3
environment:
buildbot_key_b64:
from_secret: buildbot_key_b64
@ -20,11 +20,11 @@ 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.5.3
volumes:
- name: deps
path: /root/.pub-cache
@ -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.5.3
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.5.3
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.5.3
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.5.3
volumes:
- name: deps
path: /root/.pub-cache
@ -117,7 +117,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,8 +125,7 @@ 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
@ -154,7 +153,7 @@ volumes:
temp: {}
trigger:
repo: cwtch.im/cwtch-ui
#repo: cwtch.im/cwtch-ui # allow forks to build?
branch: trunk
event:
- push
@ -175,7 +174,7 @@ clone:
steps:
- name: clone
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
environment:
buildbot_key_b64:
from_secret: buildbot_key_b64
@ -187,22 +186,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-fstable2.5.3
commands:
- powershell -command "Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.5a17/tor-win64-0.4.6.5.zip -OutFile tor.zip"
- 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-fstable2.5.3
commands:
- flutter pub get
- $Env:version += type .\VERSION
@ -213,9 +212,9 @@ 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"
@ -238,7 +237,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 +257,7 @@ steps:
- move *.sha512 deploy\$Env:builddir
- name: deploy-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
when:
event: push
status: [ success ]
@ -268,11 +267,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-11-09-18-25-v1.4.2

View File

@ -1 +1 @@
v1.1.0-2021-07-15-19-15
2021-11-09-23-25-v1.4.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.

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") {
@ -93,6 +98,65 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
.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.i("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
}
} else if (evt.EventType == "FileDownloaded") {
Log.i("FlwtchWorker", "file downloaded!");
val data = JSONObject(evt.Data);
val tempFile = data.getString("TempFile");
val fileKey = data.getString("FileKey");
if (tempFile != "") {
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.i("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 ->
@ -157,6 +221,34 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val target = (a.get("target") as? String) ?: ""
Cwtch.sendInvitation(profile, handle, target)
}
"ShareFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val filepath = (a.get("filepath") as? String) ?: ""
Cwtch.shareFile(profile, handle, filepath)
}
"DownloadFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
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, handle, 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 handle = (a.get("handle") as? String) ?: ""
val fileKey = (a.get("fileKey") as? String) ?: ""
Cwtch.verifyOrResumeDownload(profile, handle, fileKey)
}
"SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: ""
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
@ -192,25 +284,76 @@ 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 contactHandle = (a.get("handle") as? String) ?: ""
Cwtch.archiveConversation(profile, contactHandle)
}
"LeaveGroup" -> {
"DeleteContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.leaveGroup(profile, groupHandle)
val handle = (a.get("handle") as? String) ?: ""
Cwtch.deleteContact(profile, handle)
}
"RejectInvite" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.rejectInvite(profile, groupHandle)
}
"SetProfileAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setProfileAttribute(profile, key, v)
}
"SetContactAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val contact = (a.get("Contact") as? String) ?: ""
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setContactAttribute(profile, contact, key, v)
}
"Shutdown" -> {
Cwtch.shutdownCwtch();
return Result.success()
}
"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 -> return Result.failure()
}
return Result.success()
@ -268,6 +411,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,17 +12,25 @@ 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 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 +55,13 @@ 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 var dlToProfile = ""
private var dlToHandle = ""
private var dlToFileKey = ""
// 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 +83,28 @@ 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
}
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Note: this methods are invoked on the main thread.
@ -125,6 +162,18 @@ 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
}
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
@ -178,20 +227,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")

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"
}

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

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,5 +1,9 @@
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();
@ -41,12 +45,23 @@ abstract class Cwtch {
void SendInvitation(String profile, String handle, String target);
// ignore: non_constant_identifier_names
void LeaveConversation(String profile, String handle);
void ShareFile(String profile, String handle, String filepath);
// ignore: non_constant_identifier_names
void DownloadFile(String profile, String handle, String filepath, String manifestpath, String filekey);
// ignore: non_constant_identifier_names
void CreateDownloadableFile(String profile, String 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, String handle, String filekey);
// ignore: non_constant_identifier_names
void ArchiveConversation(String profile, String handle);
// ignore: non_constant_identifier_names
void DeleteContact(String profile, String 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);
@ -54,6 +69,29 @@ abstract class Cwtch {
void SetGroupAttribute(String profile, String groupHandle, String key, String value);
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle);
// ignore: non_constant_identifier_names
void SetProfileAttribute(String profile, String key, String val);
// ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, 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();

View File

@ -1,5 +1,6 @@
import 'dart:convert';
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,14 +21,16 @@ 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) {
@ -55,14 +58,32 @@ class CwtchNotifier {
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 "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;
}
@ -83,6 +104,12 @@ 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"]);
break;
@ -106,6 +133,8 @@ class CwtchNotifier {
notificationManager.notify("New Message From Peer!");
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++;
} else {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.newMarker++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
@ -144,24 +173,34 @@ class CwtchNotifier {
break;
case "NewMessageFromGroup":
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 currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages;
// 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(data["GroupID"])!.totalMessages = idx + 1;
//if not currently open
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
} else {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.newMarker++;
}
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
// 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(data["GroupID"], timestampSent.toLocal());
notificationManager.notify("New Message From Group!");
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages++;
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
// 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(data["GroupID"], timestampSent.toLocal());
notificationManager.notify("New Message From Group!");
} else {
// from me (already displayed - do not update counter)
var idx = data["Signature"];
@ -179,10 +218,15 @@ class CwtchNotifier {
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"]);
var total = int.parse(data["Data"]);
if (total != profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages) {
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = total;
}
break;
case "SendMessageToPeerError":
// Ignore
break;
case "IndexedFailure":
EnvironmentConfig.debugLog("IndexedFailure");
var idx = data["Index"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])?.getMessageKey(idx);
try {
@ -249,7 +293,7 @@ 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;
}
@ -289,12 +333,31 @@ class CwtchNotifier {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.nickname = data["Data"];
}
} else if (data["Key"] == "local.archived") {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.isArchived = data["Data"] == "true";
}
} else {
EnvironmentConfig.debugLog("unhandled set group attribute event: ${data['Key']}");
}
break;
case "SetPeerAttribute":
if (data["Key"] == "local.name") {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.nickname = data["Data"];
}
} else if (data["Key"] == "local.archived") {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.isArchived = data["Data"] == "true";
}
} else if (data["Key"] == "LastKnowSignature") {
// group syncing information that isn't relevant to the UI...
} else {
EnvironmentConfig.debugLog("unhandled set peer attribute event: ${data['Key']}");
}
break;
case "NewRetValMessageFromPeer":
if (data["Path"] == "name") {
if (data["Path"] == "name" && data["Data"].toString().trim().length > 0) {
// 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"];
@ -303,6 +366,20 @@ class CwtchNotifier {
EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}");
}
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;
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,14 @@ 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);
typedef void_from_string_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, 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,16 +51,9 @@ 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 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);
@ -64,22 +65,35 @@ typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, 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 library_path = getLibraryPath();
if (library_path == 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(library_path);
cwtchNotifier = _cwtchNotifier;
}
@ -88,8 +102,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 +117,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 +189,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 +203,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");
@ -184,6 +240,7 @@ class CwtchFfi implements Cwtch {
final SelectProfile = selectProfileC.asFunction<GetJsonBlobStringFn>();
final ut8Onion = onion.toNativeUtf8();
SelectProfile(ut8Onion, ut8Onion.length);
malloc.free(ut8Onion);
}
// ignore: non_constant_identifier_names
@ -194,6 +251,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,6 +262,7 @@ 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
@ -214,6 +274,9 @@ class CwtchFfi implements Cwtch {
final utf8handle = handle.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
malloc.free(utf8handle);
return jsonMessage;
}
@ -226,6 +289,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,6 +301,7 @@ class CwtchFfi implements Cwtch {
final SendAppBusEvent = sendAppBusEvent.asFunction<StringFn>();
final utf8json = json.toNativeUtf8();
SendAppBusEvent(utf8json, utf8json.length);
malloc.free(utf8json);
}
@override
@ -247,6 +313,8 @@ class CwtchFfi implements Cwtch {
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
AcceptContact(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
@ -258,6 +326,8 @@ class CwtchFfi implements Cwtch {
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
BlockContact(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
@ -270,6 +340,9 @@ class CwtchFfi implements Cwtch {
final u2 = contactHandle.toNativeUtf8();
final u3 = message.toNativeUtf8();
SendMessage(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
}
@override
@ -282,8 +355,80 @@ class CwtchFfi implements Cwtch {
final u2 = contactHandle.toNativeUtf8();
final u3 = target.toNativeUtf8();
SendInvitation(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 ShareFile(String profileOnion, String contactHandle, String filepath) {
var shareFile = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_ShareFile");
// ignore: non_constant_identifier_names
final ShareFile = shareFile.asFunction<VoidFromStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filepath.toNativeUtf8();
ShareFile(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 DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) {
var dlFile = library.lookup<NativeFunction<void_from_string_string_string_string_string_function>>("c_DownloadFile");
// ignore: non_constant_identifier_names
final DownloadFile = dlFile.asFunction<VoidFromStringStringStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filepath.toNativeUtf8();
final u4 = manifestpath.toNativeUtf8();
final u5 = filekey.toNativeUtf8();
DownloadFile(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length, u5, u5.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
malloc.free(u4);
malloc.free(u5);
}
@override
// ignore: non_constant_identifier_names
void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) {
// 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, String contactHandle, String filekey) {
var fn = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_VerifyOrResumeDownload");
// ignore: non_constant_identifier_names
final VerifyOrResumeDownload = fn.asFunction<VoidFromStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filekey.toNativeUtf8();
VerifyOrResumeDownload(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 ResetTor() {
@ -302,6 +447,8 @@ 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
@ -315,6 +462,10 @@ class CwtchFfi implements Cwtch {
final u3 = key.toNativeUtf8();
final u4 = value.toNativeUtf8();
SetGroupAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
malloc.free(u4);
}
@override
@ -326,9 +477,12 @@ class CwtchFfi implements Cwtch {
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
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,31 +491,40 @@ 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, String handle) {
var archiveConversation = library.lookup<NativeFunction<string_string_to_void_function>>("c_ArchiveConversation");
// ignore: non_constant_identifier_names
final LeaveConversation = leaveConversation.asFunction<VoidFromStringStringFn>();
final ArchiveConversation = archiveConversation.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = handle.toNativeUtf8();
LeaveConversation(u1, u1.length, u2, u2.length);
ArchiveConversation(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@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, String handle) {
var deleteContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteContact");
// ignore: non_constant_identifier_names
final LeaveGroup = leaveGroup.asFunction<VoidFromStringStringFn>();
final DeleteContact = deleteContact.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
LeaveGroup(u1, u1.length, u2, u2.length);
final u2 = handle.toNativeUtf8();
DeleteContact(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
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
@ -369,6 +532,8 @@ class CwtchFfi implements Cwtch {
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags);
malloc.free(utf8profile);
malloc.free(utf8handle);
}
@override
@ -380,14 +545,151 @@ 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 = key.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 SetContactAttribute(String profile, String contact, String key, String val) {
var setContactAttribute = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_SetContactAttribute");
// ignore: non_constant_identifier_names
final SetContactAttribute = setContactAttribute.asFunction<VoidFromStringStringStringStringFn>();
final u1 = profile.toNativeUtf8();
final u2 = contact.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = key.toNativeUtf8();
SetContactAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u2);
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,6 +702,7 @@ class CwtchFfi implements Cwtch {
}
@override
// ignore: non_constant_identifier_names
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
@ -409,6 +712,20 @@ class CwtchFfi implements Cwtch {
final utf8contentHash = contentHash.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
malloc.free(utf8handle);
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);
}
}

View File

@ -131,6 +131,35 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target});
}
@override
// ignore: non_constant_identifier_names
void ShareFile(String profileOnion, String contactHandle, String filepath) {
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath});
}
@override
// ignore: non_constant_identifier_names
void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) {
cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey});
}
// ignore: non_constant_identifier_names
void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) {
cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey});
}
@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, String contactHandle, String filekey) {
cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "handle": contactHandle, "filekey": filekey});
}
@override
// ignore: non_constant_identifier_names
void ResetTor() {
@ -162,23 +191,89 @@ 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, String handle) {
cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "handle": handle});
}
@override
// ignore: non_constant_identifier_names
void LeaveConversation(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("LeaveConversation", {"ProfileOnion": profileOnion, "contactHandle": contactHandle});
void ArchiveConversation(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "handle": contactHandle});
}
@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});
cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "midx": index, "flags": flags});
}
@override
// 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 SetContactAttribute(String profile, String contact, String key, String val) {
cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "Contact": contact, "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
Future<void> Shutdown() async {
print("gomobile.dart Shutdown");
cwtchPlatform.invokeMethod("Shutdown", {});

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

@ -21,6 +21,27 @@ 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;
notifyListeners();
}
/// Called by the event bus.
handleUpdate(String error) {
var parts = error.split(".");
@ -37,6 +58,8 @@ class ErrorHandler extends ChangeNotifier {
case deleteProfileErrorPrefix:
handleDeleteProfileError(errorType);
break;
case deletedServerErrorPrefix:
handleDeletedServerError(errorType);
}
notifyListeners();
@ -91,4 +114,19 @@ class ErrorHandler extends ChangeNotifier {
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,66 @@
{
"@@locale": "de",
"@@last_modified": "2021-07-14T23:49:07+02:00",
"@@last_modified": "2021-11-11T01:02:08+01:00",
"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": "Herunterladen",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"addPeerTab": "Einen anderen Nutzer hinzufügen",
"addPeer": "Anderen Nutzer hinzufügen",
"peerNotOnline": "Der andere Nutzer ist offline. Die App kann momentan nicht verwendet werden.",
"peerBlockedMessage": "Anderer Nutzer ist blockiert",
"peerOfflineMessage": "Anderer Nutzer ist offline, Nachrichten können derzeit nicht zugestellt werden",
"blockBtn": "Anderen Nutzer blockieren",
"savePeerHistory": "Peer-Verlauf speichern",
"savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.",
"dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen",
"unblockBtn": "Anderen Nutzer entsperren",
"blockUnknownLabel": "Unbekannte Peers blockieren",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
@ -84,7 +144,6 @@
"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",
@ -104,7 +163,6 @@
"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",
@ -128,7 +186,6 @@
"password1Label": "Passwort",
"currentPasswordLabel": "aktuelles Passwort",
"yourDisplayName": "Dein Anzeigename",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"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",
@ -141,11 +198,6 @@
"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",
"addressLabel": "Adresse",
@ -158,15 +210,12 @@
"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",
"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",
@ -192,7 +241,6 @@
"newBulletinLabel": "Neue Meldung",
"joinGroup": "Gruppe beitreten",
"createGroup": "Gruppe erstellen",
"addPeer": "Anderen Nutzer hinzufügen",
"groupAddr": "Adresse",
"invitation": "Einladung",
"server": "Server",
@ -201,7 +249,6 @@
"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,66 @@
{
"@@locale": "en",
"@@last_modified": "2021-07-14T23:49:07+02:00",
"@@last_modified": "2021-11-11T01:02:08+01:00",
"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",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
"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...",
@ -84,7 +144,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",
@ -104,7 +163,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",
@ -128,7 +186,6 @@
"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",
@ -141,11 +198,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",
@ -158,15 +210,12 @@
"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.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Invite",
@ -192,7 +241,6 @@
"newBulletinLabel": "New Bulletin",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
@ -201,7 +249,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,66 @@
{
"@@locale": "es",
"@@last_modified": "2021-07-14T23:49:07+02:00",
"@@last_modified": "2021-11-11T01:02:08+01:00",
"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": "Envía esta dirección a los contactos con los que quieras conectarte",
"addPeerTab": "Agregar Contacto",
"addPeer": "Agregar Contacto",
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
"peerBlockedMessage": "Contacto bloqueado",
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
"blockBtn": "Bloquear contacto",
"savePeerHistory": "Guardar el historial con contacto",
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
"dontSavePeerHistory": "Eliminar historial de contacto",
"unblockBtn": "Desbloquear contacto",
"blockUnknownLabel": "Bloquear conexiones desconocidas",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"networkStatusConnecting": "Conectando a la red y a los contactos...",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
@ -84,7 +144,6 @@
"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",
@ -104,7 +163,6 @@
"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",
@ -128,7 +186,6 @@
"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",
@ -141,11 +198,6 @@
"editProfileTitle": "Editar perfil",
"addProfileTitle": "Agregar nuevo perfil",
"deleteBtn": "Eliminar",
"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",
@ -158,15 +210,12 @@
"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",
@ -192,7 +241,6 @@
"newBulletinLabel": "Nuevo Boletín",
"joinGroup": "Únete al grupo",
"createGroup": "Crear perfil",
"addPeer": "Agregar Contacto",
"groupAddr": "Dirección",
"invitation": "Invitación",
"server": "Servidor",
@ -201,7 +249,6 @@
"peerAddress": "Dirección",
"joinGroupTab": "Únete a un grupo",
"createGroupTab": "Crear un grupo",
"addPeerTab": "Agregar Contacto",
"createGroupBtn": "Crear",
"defaultGroupName": "El Grupo Asombroso",
"createGroupTitle": "Crear un grupo"

View File

@ -1,6 +1,66 @@
{
"@@locale": "fr",
"@@last_modified": "2021-07-14T23:49:07+02:00",
"@@last_modified": "2021-11-11T01:02:08+01:00",
"newMessagesLabel": "New Messages",
"localeRU": "Russian",
"copyServerKeys": "Copier les clés",
"verfiyResumeButton": "Vérifier\/reprendre",
"fileCheckingStatus": "Vérification de l'état du téléchargement",
"fileInterrupted": "Interrompu",
"fileSavedTo": "Enregistré dans",
"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.",
"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.",
"deleteServerConfirmBtn": "Supprimer vraiment le serveur",
"deleteServerSuccess": "Le serveur a été supprimé avec succès",
"enterCurrentPasswordForDeleteServer": "Veuillez saisir le mot de passe actuel pour supprimer ce serveur",
"copyAddress": "Copier l'adresse",
"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",
"enterServerPassword": "Entrez le mot de passe pour déverrouiller le serveur",
"unlockProfileTip": "Veuillez créer ou déverrouiller un profil pour commencer !",
"unlockServerTip": "Veuillez créer ou déverrouiller un serveur pour commencer !",
"addServerTooltip": "Ajouter un nouveau serveur",
"serversManagerTitleShort": "Serveurs",
"serversManagerTitleLong": "Serveurs que vous hébergez",
"saveServerButton": "Enregistrer le serveur",
"serverAutostartDescription": "Contrôle si l'application lance automatiquement le serveur au démarrage.",
"serverAutostartLabel": "Démarrage automatique",
"serverEnabledDescription": "Démarrer ou arrêter le serveur",
"serverEnabled": "Serveur activé",
"serverDescriptionDescription": "Votre description du serveur est à des fins de gestion personnelle uniquement, elle ne sera jamais partagée.",
"serverDescriptionLabel": "Description du serveur",
"serverAddress": "Adresse du serveur",
"editServerTitle": "Modifier le 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.",
"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.",
"settingFileSharing": "Partage de fichiers",
"tooltipSendFile": "Envoyer le fichier",
"messageFileOffered": "Contact vous propose de vous envoyer un fichier",
"messageFileSent": "Vous avez envoyé 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",
"openFolderButton": "Ouvrir le dossier",
"retrievingManifestMessage": "Récupération des informations sur le fichier...",
"streamerModeLabel": "Mode Streamer\/Présentation",
"archiveConversation": "Archiver cette conversation",
"profileOnionLabel": "Envoyez cette adresse aux personnes avec lesquelles vous souhaitez entrer en contact.",
"addPeerTab": "Ajouter un contact",
"addPeer": "Ajouter le 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",
"savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au contact.",
"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",
"networkStatusConnecting": "Connexion au réseau et aux contacts...",
"showMessageButton": "Afficher le message",
"blockedMessageMessage": "Ce message provient d'un profil que vous avez bloqué.",
"placeholderEnterMessage": "saisissez un message",
@ -84,7 +144,6 @@
"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",
@ -104,7 +163,6 @@
"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",
@ -128,7 +186,6 @@
"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",
@ -141,11 +198,6 @@
"editProfileTitle": "Modifier le profil",
"addProfileTitle": "Ajouter un nouveau profil",
"deleteBtn": "Effacer",
"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",
@ -158,15 +210,12 @@
"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é",
"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",
@ -192,7 +241,6 @@
"newBulletinLabel": "Nouveau bulletin",
"joinGroup": "Rejoindre le groupe",
"createGroup": "Créer un groupe",
"addPeer": "Ajouter un pair",
"groupAddr": "Adresse",
"invitation": "Invitation",
"server": "Serveur",
@ -201,7 +249,6 @@
"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,6 +1,66 @@
{
"@@locale": "it",
"@@last_modified": "2021-07-14T23:49:07+02:00",
"@@last_modified": "2021-11-11T01:02:08+01:00",
"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": "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.",
"settingFileSharing": "Condivisione file",
"tooltipSendFile": "Invia file",
"messageFileOffered": "Il contatto offre l'invio di un 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": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
"addPeerTab": "Aggiungi un peer",
"addPeer": "Aggiungi peer",
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
"peerBlockedMessage": "Il peer è bloccato",
"peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
"blockBtn": "Blocca il peer",
"savePeerHistory": "Salva cronologia peer",
"savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
"dontSavePeerHistory": "Elimina cronologia dei peer",
"unblockBtn": "Sblocca il peer",
"blockUnknownLabel": "Blocca peer sconosciuti",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"networkStatusConnecting": "Connessione alla rete e ai peer ...",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
@ -84,7 +144,6 @@
"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",
@ -104,7 +163,6 @@
"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",
@ -128,7 +186,6 @@
"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",
@ -141,11 +198,6 @@
"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",
@ -158,15 +210,12 @@
"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",
@ -192,7 +241,6 @@
"newBulletinLabel": "Nuovo bollettino",
"joinGroup": "Unisciti al gruppo",
"createGroup": "Crea un gruppo",
"addPeer": "Aggiungi peer",
"groupAddr": "Indirizzo",
"invitation": "Invito",
"server": "Server",
@ -201,7 +249,6 @@
"peerAddress": "Indirizzo",
"joinGroupTab": "Unisciti a un gruppo",
"createGroupTab": "Crea un gruppo",
"addPeerTab": "Aggiungi un peer",
"createGroupBtn": "Crea",
"defaultGroupName": "Gruppo fantastico",
"createGroupTitle": "Crea un gruppo"

View File

@ -1,6 +1,66 @@
{
"@@locale": "pl",
"@@last_modified": "2021-07-14T23:49:07+02:00",
"@@last_modified": "2021-11-11T01:02:08+01:00",
"newMessagesLabel": "New Messages",
"localeRU": "Russian",
"copyServerKeys": "Kopiuj klucze",
"verfiyResumeButton": "Zweryfikuj\/wznów",
"fileCheckingStatus": "Sprawdzanie stanu pobierania",
"fileInterrupted": "Przerwane",
"fileSavedTo": "Zapisano do",
"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": "Naprawdę usuń serwer",
"deleteServerSuccess": "Pomyślnie usunięto serwer",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
"copyAddress": "Skopiuj adres",
"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": "Profile",
"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": "Eksperyment udostępniania plików pozwala na wysyłanie i odbieranie plików od kontaktów i grup Cwtch. Zauważ, że udostępnienie pliku grupie spowoduje, że członkowie tej grupy połączą się z Tobą bezpośrednio przez Cwtch, aby go pobrać.",
"settingFileSharing": "Udostępnianie plików",
"tooltipSendFile": "Wyślij plik",
"messageFileOffered": "Kontakt proponuje wysłanie Ci pliku",
"messageFileSent": "Plik został wysłany",
"messageEnableFileSharing": "Włącz eksperyment udostępniania plików, aby wyświetlić tę wiadomość.",
"labelFilesize": "Rozmiar",
"labelFilename": "Nazwa pliku",
"downloadFileButton": "Pobierz",
"openFolderButton": "Otwórz folder",
"retrievingManifestMessage": "Pobieranie informacji o pliku...",
"streamerModeLabel": "Tryb streamera\/prezentacji",
"archiveConversation": "Zarchiwizuj tę rozmowę",
"profileOnionLabel": "Send this address to contacts 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",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
"dontSavePeerHistory": "Delete History",
"unblockBtn": "Unblock Contact",
"blockUnknownLabel": "Block Unknown Contacts",
"blockUnknownConnectionsEnabledDescription": "Połączenia od nieznanych kontaktów są blokowane. Można to zmienić w Ustawieniach",
"networkStatusConnecting": "Connecting to network and contacts...",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
@ -84,7 +144,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",
@ -104,7 +163,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",
@ -128,7 +186,6 @@
"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",
@ -141,11 +198,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",
@ -158,15 +210,12 @@
"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.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Invite",
@ -192,7 +241,6 @@
"newBulletinLabel": "New Bulletin",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
@ -201,7 +249,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,66 @@
{
"@@locale": "pt",
"@@last_modified": "2021-07-14T23:49:07+02:00",
"@@last_modified": "2021-11-11T01:02:08+01:00",
"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 contacts 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",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
"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...",
@ -84,7 +144,6 @@
"todoPlaceholder": "Afazer…",
"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",
@ -104,7 +163,6 @@
"localeFr": "Frances",
"localeEn": "English",
"settingLanguage": "Language",
"blockUnknownLabel": "Block Unknown Peers",
"zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)",
"versionBuilddate": "Version: %1 Built on: %2",
"cwtchSettingsTitle": "Configurações do Cwtch",
@ -128,7 +186,6 @@
"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",
@ -141,11 +198,6 @@
"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",
"saveBtn": "Salvar",
"displayNameLabel": "Nome de Exibição",
"addressLabel": "Endereço",
@ -158,15 +210,12 @@
"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",
"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.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Convidar",
@ -192,7 +241,6 @@
"newBulletinLabel": "Novo Boletim",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
@ -201,7 +249,6 @@
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Criar",
"defaultGroupName": "Grupo incrível",
"createGroupTitle": "Criar Grupo"

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

@ -0,0 +1,254 @@
{
"@@locale": "ru",
"@@last_modified": "2021-11-10T18:47:30+01:00",
"localeRU": "Russian",
"copyServerKeys": "Копировать ключи",
"verfiyResumeButton": "Проверить\/продолжить",
"fileCheckingStatus": "Проверка статуса загрузки",
"fileInterrupted": "Прервано",
"fileSavedTo": "Сохранить в",
"plainServerDescription": "Мы настоятельно рекомендуем защитить свой сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.",
"encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.",
"deleteServerConfirmBtn": "Точно удалить сервер?",
"deleteServerSuccess": "Сервер успешно удален",
"enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его",
"copyAddress": "Копировать адрес",
"settingServersDescription": "Экспериментальная функция которая позволяет добавлять сервер Cwtch. В меню появится дополнительная опция Серверы",
"settingServers": "Использовать серверы",
"enterServerPassword": "Введите пароль для разблокировки сервера",
"unlockProfileTip": "Создайте или разблокируйте профиль, чтобы начать",
"unlockServerTip": "Создайте или разблокируйте сервер, чтобы начать",
"addServerTooltip": "Добавить сервер",
"serversManagerTitleShort": "Серверы",
"serversManagerTitleLong": "Личные серверы",
"saveServerButton": "Сохранить сервер",
"serverAutostartDescription": "Автозапуск сервера при старте программы",
"serverAutostartLabel": "Автозапуск",
"serverEnabledDescription": "Запустить или остановить сервер",
"serverEnabled": "Сервер запущен",
"serverDescriptionDescription": "Описание видите только Вы. Сделано для удобства",
"serverDescriptionLabel": "Описание сервера",
"serverAddress": "Адрес сервера",
"editServerTitle": "Изменить сервер",
"addServerTitle": "Добавить сервер",
"titleManageProfilesShort": "Профили",
"descriptionStreamerMode": "При включении этого параметра, внешний вид некоторых элементов становится более приватным, скрывая длинные Onion адреса и адреса контактов, оставляя только заданные имена",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch.",
"settingFileSharing": "Передача файлов",
"tooltipSendFile": "Отправить файл",
"messageFileOffered": "Контакт предлагает загрузить вам файл",
"messageFileSent": "Вы отправили файл",
"messageEnableFileSharing": "Включите экспериментальную функцию Обмен файлами чтобы просмотреть это сообщение.",
"labelFilesize": "Размер",
"labelFilename": "Имя-файла",
"downloadFileButton": "Загрузить",
"openFolderButton": "Открыть папку",
"retrievingManifestMessage": "Получение информации о файле...",
"streamerModeLabel": "Режим презентации",
"archiveConversation": "Отправить чат в архив",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"addPeerTab": "Добавить контакт",
"addPeer": "Добавить контакт",
"peerNotOnline": "Контакт не в сети. Вы не можете связаться с ним пока он не появиться в сети.",
"peerBlockedMessage": "Контакт заблокирован",
"peerOfflineMessage": "Контакт не в сети, сообщения не могут быть отправлены",
"blockBtn": "Заблокировать контакт",
"savePeerHistory": "Хранить исторую",
"savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.",
"dontSavePeerHistory": "Удалить историю",
"unblockBtn": "Разблокировать контакт",
"blockUnknownLabel": "Блокировать неизвестные контакты",
"blockUnknownConnectionsEnabledDescription": "Соединения от неизвестных контактов блокируются. Данный параметр можно изменить в настройках",
"networkStatusConnecting": "Подключение к сети и контактам...",
"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": "Показать пароль",
"serverNotSynced": "Синхронизация новых сообщений (это может занять некоторое время)...",
"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": "Вас пригласили присоединиться к группе:",
"pasteAddressToAddContact": "Вставьте адрес cwtch, приглашение или пакет ключей здесь, чтобы добавить их в контакты",
"tooltipAddContact": "Добавление нового контакта или разговора",
"titleManageContacts": "Разговоры",
"titleManageServers": "Управление серверами",
"dateNever": "Никогда",
"dateLastYear": "Прошлый год",
"dateYesterday": "Вчера",
"dateLastMonth": "Прошлый месяц",
"dateRightNow": "Прямо сейчас",
"successfullAddedContact": "Успешно добавлен",
"descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей не состоящих в ваших контактах будут отклонены.",
"descriptionExperimentsGroups": "Данная экспериментальная функция позволяет Cwtch подключаться к недоверенной серверной инфраструктуре, чтобы облегчить Вам общение с более чем одним контактом.",
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный част 1 на 1..",
"titleManageProfiles": "Управление профилями Cwtch",
"tooltipUnlockProfiles": "Разблокировать зашифрованные профили, введя их пароль.",
"tooltipOpenSettings": "Откройте панель настроек",
"invalidImportString": "Недействительная строка импорта",
"contactAlreadyExists": "Контакт уже существует",
"conversationSettings": "Настройки чата",
"enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.",
"enableGroups": "Включить Групповые чаты",
"experimentsEnabled": "Включить Экспериментальные функции",
"localeIt": "Итальянский",
"localeEs": "Испанский",
"addListItem": "Добавить новый элемент",
"addNewItem": "Добавить новый элемент в список",
"todoPlaceholder": "Выполняю...",
"newConnectionPaneTitle": "Новое соединение",
"networkStatusOnline": "Online",
"networkStatusAttemptingTor": "Попытка подключиться к сети Tor",
"networkStatusDisconnected": "Нет сети. Проверьте подключение к интернету",
"viewGroupMembershipTooltip": "Просмотр членства в группе",
"loadingTor": "Загрузка Tor...",
"smallTextLabel": "Маленький",
"defaultScalingText": "Размер текста по умолчанию (коэффициент масштабирования:",
"builddate": "Построен на: %2",
"version": "Версия %1",
"versionTor": "Версия %1 c tor %2",
"themeDark": "Темная",
"themeLight": "Светлая",
"settingTheme": "Тема",
"largeTextLabel": "Большой",
"settingInterfaceZoom": "Уровень масштабирования",
"localeDe": "Немецкий",
"localePt": "Португальский",
"localeFr": "Французский",
"localeEn": "Английский",
"settingLanguage": "Язык",
"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": "Пароль",
"copiedToClipboardNotification": "Copied to Clipboard",
"copyBtn": "Copy",
"editProfile": "Изменить профиль",
"newProfile": "Новый профиль",
"defaultProfileName": "Alice",
"profileName": "Отображаемое имя",
"editProfileTitle": "Изменить профиль",
"addProfileTitle": "Добавить новый профиль",
"deleteBtn": "Delete",
"saveBtn": "Save",
"displayNameLabel": "Отображаемое имя",
"addressLabel": "Адрес",
"puzzleGameBtn": "Puzzle Game",
"bulletinsBtn": "Bulletins",
"listsBtn": "Списки",
"chatBtn": "Чат",
"rejectGroupBtn": "Отклонить",
"acceptGroupBtn": "Принять",
"acceptGroupInviteLabel": "Хотите принять приглашение в",
"newGroupBtn": "Создать новую группу",
"copiedClipboardNotification": "Скопировано в буфер обмена",
"pendingLabel": "Ожидаемый",
"acknowledgedLabel": "Отправлено",
"couldNotSendMsgError": "Не удалось отправить это сообщение",
"dmTooltip": "Нажмите, чтобы перейти в DM",
"membershipDescription": "Ниже приведен список пользователей, отправивших сообщения группе. Этот список может не отражать всех пользователей, имеющих доступ к группе.",
"addListItemBtn": "Добавить элемент",
"searchList": "Список поиска",
"update": "Обновить",
"inviteBtn": "Пригласить",
"inviteToGroupLabel": "Пригласить в группу",
"groupNameLabel": "Group name",
"viewServerInfo": "Информация о сервере",
"serverSynced": "Синхронизировано",
"serverConnectivityDisconnected": "Сервер отключен",
"serverConnectivityConnected": "Сервер подключен",
"serverInfo": "Информация о сервере",
"invitationLabel": "Приглашение",
"serverLabel": "Server",
"search": "Поиск...",
"cycleColoursDesktop": "Нажмите, чтобы переключать цвета.\nПравый клик чтобы сбросить.",
"cycleColoursAndroid": "Нажмите, чтобы переключать цвета.\nНажмите и удерживайте, чтобы сбросить.",
"cycleMorphsDesktop": "Нажмите, чтобы просмотреть формы.\nПравый клик чтобы сбросить.",
"cycleMorphsAndroid": "Нажмите, чтобы просмотреть формы.\nНажмите и удерживайте, чтобы сбросить.",
"cycleCatsDesktop": "Нажмите, чтобы просмотреть категории.\nПравый клик чтобы сбросить.",
"cycleCatsAndroid": "Нажмите, чтобы просмотреть категории.\nНажмите и удерживайте, чтобы сбросить.",
"blocked": "Заблокировано",
"titlePlaceholder": "заговолок...",
"postNewBulletinLabel": "Опубликовать новый бюллетень",
"newBulletinLabel": "Новый бюллетень",
"joinGroup": "Вступить в группу",
"createGroup": "Создать группу",
"groupAddr": "Адрес",
"invitation": "Приглашение",
"server": "Сервер",
"groupName": "Имя группы",
"peerName": "Имя",
"peerAddress": "Адрес",
"joinGroupTab": "Присоединиться к группе",
"createGroupTab": "Создать группу",
"createGroupBtn": "Создать",
"defaultGroupName": "Замечательная группа",
"createGroupTitle": "Создать группу"
}

View File

@ -17,6 +17,7 @@ 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;
@ -27,6 +28,7 @@ var globalSettings = Settings(Locale("en", ''), OpaqueDark());
var globalErrorHandler = ErrorHandler();
var globalTorStatus = TorStatus();
var globalAppState = AppState();
var globalServersList = ServerListState();
void main() {
print("Cwtch version: ${EnvironmentConfig.BUILD_VER} built on: ${EnvironmentConfig.BUILD_DATE}");
@ -62,13 +64,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()");
@ -82,6 +84,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) {
@ -94,6 +97,7 @@ class FlwtchState extends State<Flwtch> {
getErrorHandlerProvider(),
getTorStatusProvider(),
getAppStateProvider(),
getServerListStateProvider(),
],
builder: (context, widget) {
return Consumer2<Settings, AppState>(
@ -116,13 +120,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...
@ -156,7 +160,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);
}
@ -169,7 +173,7 @@ class FlwtchState extends State<Flwtch> {
var args = jsonDecode(call.arguments);
var profile = profs.getProfile(args["ProfileOnion"])!;
var convo = profile.contactList.getContact(args["Handle"])!;
var initialIndex = convo.unreadMessages;
Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages;
convo.unreadMessages = 0;
// single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation
@ -187,7 +191,7 @@ class FlwtchState extends State<Flwtch> {
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: convo),
],
builder: (context, child) => MessageView(initialIndex),
builder: (context, child) => MessageView(),
);
},
),

View File

@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/models/profileservers.dart';
////////////////////
/// UI State ///
@ -30,6 +30,8 @@ class AppState extends ChangeNotifier {
String appError = "";
String? _selectedProfile;
String? _selectedConversation;
int _initialScrollIndex = 0;
int _hoveredIndex = -1;
int? _selectedIndex;
bool _unreadMessagesBelow = false;
@ -61,12 +63,26 @@ class AppState extends ChangeNotifier {
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;
}
@ -135,6 +151,9 @@ class ContactListState extends ChangeNotifier {
// 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;
// special sorting for contacts with no messages in either history
if (a.lastMessageTime.millisecondsSinceEpoch == 0 && b.lastMessageTime.millisecondsSinceEpoch == 0) {
// online contacts first
@ -188,12 +207,13 @@ class ContactListState extends ChangeNotifier {
class ProfileInfoState extends ChangeNotifier {
ContactListState _contacts = ContactListState();
ServerListState _servers = ServerListState();
ProfileServerListState _servers = ProfileServerListState();
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.
@ -228,6 +248,7 @@ class ProfileInfoState extends ChangeNotifier {
numUnread: contact["numUnread"],
isGroup: contact["isGroup"],
server: contact["groupServer"],
archived: contact["isArchived"] == true,
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])));
}));
@ -246,7 +267,7 @@ 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"], status: server["status"]);
}));
notifyListeners();
}
@ -295,7 +316,7 @@ class ProfileInfoState extends ChangeNotifier {
}
ContactListState get contactList => this._contacts;
ServerListState get serverList => this._servers;
ProfileServerListState get serverList => this._servers;
@override
void dispose() {
@ -336,6 +357,128 @@ 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)) {
if (progress < 0) {
this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
this._downloads[fileKey]!.interrupted = true;
notifyListeners();
} else {
print("error: received progress for unknown download " + fileKey);
}
} 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)) {
print("error: received download completion notice for unknown download " + fileKey);
} else {
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);
}
this._downloads[fileKey]!.timeEnd = DateTime.now();
this._downloads[fileKey]!.downloadedTo = finalPath;
this._downloads[fileKey]!.complete = true;
notifyListeners();
}
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 }
@ -364,25 +507,26 @@ class ContactInfoState extends ChangeNotifier {
late int _totalMessages = 0;
late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageRowState>> keys;
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,
authorization = ContactAuthorization.unknown,
status = "",
imagePath = "",
savePeerHistory = "DeleteHistoryConfirmed",
numMessages = 0,
numUnread = 0,
lastMessageTime,
server,
}) {
ContactInfoState(this.profileOnion, 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._authorization = authorization;
@ -393,12 +537,24 @@ class ContactInfoState extends ChangeNotifier {
this._savePeerHistory = savePeerHistory;
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
this._server = server;
this._archived = archived;
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();
@ -433,10 +589,36 @@ 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;

View File

@ -4,6 +4,7 @@ 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';
@ -14,6 +15,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
@ -33,38 +35,39 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, String
try {
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, 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, contactHandle, index, DateTime.now(), "", "", null, 0, false, true);
try {
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());
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'];
}
metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error);
dynamic message = jsonDecode(messageWrapper['Message']);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
@ -77,11 +80,14 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, String
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) {
print("an error! " + e.toString());
return MalformedMessage(metadata);
}
});

View File

@ -0,0 +1,76 @@
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 '../../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) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
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;
if (!validHash(rootHash, nonce)) {
return MessageRow(MalformedBubble());
}
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
});
}
@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 FileBubble(
nameSuggestion,
rootHash,
nonce,
fileSize,
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

@ -21,8 +21,7 @@ class InviteMessage 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();
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
String inviteTarget;
String inviteNick;
String invite = this.content;

View File

@ -94,7 +94,7 @@ 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();
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
return MessageRow(
QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
if (localIndex != null) {

View File

@ -32,7 +32,7 @@ class TextMessage 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();
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
return MessageRow(MessageBubble(this.content), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
});
}

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class ProfileServerListState extends ChangeNotifier {
List<RemoteServerInfoState> _servers = [];
void replace(Iterable<RemoteServerInfoState> newServers) {
_servers.clear();
_servers.addAll(newServers);
notifyListeners();
}
RemoteServerInfoState? getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null;
}
void updateServerCache(String onion, String status) {
int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_servers[idx] = RemoteServerInfoState(onion: onion, status: status);
} else {
print("Tried to update server cache without a starting state...this is probably an error");
}
notifyListeners();
}
List<RemoteServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
}
class RemoteServerInfoState extends ChangeNotifier {
final String onion;
final String status;
RemoteServerInfoState({required this.onion, required this.status});
}

View File

@ -9,28 +9,72 @@ 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 delete(String onion) {
_servers.removeWhere((element) => element.onion == onion);
notifyListeners();
}
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;
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();
}
}

View File

@ -374,7 +374,7 @@ class OpaqueDark extends OpaqueThemeType {
}
Color defaultButtonDisabledColor() {
return deepPurple;
return lightGrey;
}
Color defaultButtonDisabledTextColor() {
@ -630,7 +630,7 @@ class OpaqueLight extends OpaqueThemeType {
static final Color whitePurple = Color(0xFFFFFDFF);
static final Color softPurple = Color(0xFFFDF3FC);
static final Color purple = Color(0xFFDFB9DE);
static final Color brightPurple = Color(0xFF760388);
static final Color brightPurple = Color(0xFFD1B0E0);
static final Color darkPurple = Color(0xFF350052);
static final Color greyPurple = Color(0xFF775F84);
static final Color pink = Color(0xFFE85DA1);
@ -684,7 +684,7 @@ class OpaqueLight extends OpaqueThemeType {
}
Color defaultButtonDisabledColor() {
return purple;
return lightGrey;
}
Color defaultButtonDisabledTextColor() {
@ -900,11 +900,11 @@ class OpaqueLight extends OpaqueThemeType {
}
Color messageFromMeBackgroundColor() {
return darkPurple;
return brightPurple;
}
Color messageFromMeTextColor() {
return whitePurple;
return mainTextColor();
}
Color messageFromOtherBackgroundColor() {
@ -948,11 +948,14 @@ ThemeData mkThemeData(Settings opaque) {
backgroundColor: opaque.current().backgroundMainColor(),
highlightColor: opaque.current().hilightElementTextColor(),
iconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
color: opaque.current().toolbarIconColor(),
),
cardColor: opaque.current().backgroundMainColor(),
appBarTheme: AppBarTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
iconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
titleTextStyle: TextStyle(
color: opaque.current().mainTextColor(),
),
@ -969,9 +972,15 @@ ThemeData mkThemeData(Settings opaque) {
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
backgroundColor: MaterialStateProperty.resolveWith((states) => states.contains(MaterialState.disabled) ? opaque.current().defaultButtonDisabledColor() : opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
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),
@ -1004,7 +1013,11 @@ ThemeData mkThemeData(Settings opaque) {
thumbColor: MaterialStateProperty.all(opaque.current().mainTextColor()),
trackColor: MaterialStateProperty.all(opaque.current().dropShadowColor()),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(backgroundColor: opaque.current().defaultButtonColor(), hoverColor: opaque.current().defaultButtonActiveColor()),
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()),
);

View File

@ -9,6 +9,9 @@ import 'opaque.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
const TapirGroupsExperiment = "tapir-groups-experiment";
const ServerManagementExperiment = "servers-experiment";
const FileSharingExperiment = "filesharing";
const ClickableLinksExperiment = "clickable-links";
enum DualpaneMode {
Single,
@ -31,6 +34,7 @@ class Settings extends ChangeNotifier {
DualpaneMode _uiColumnModeLandscape = DualpaneMode.CopyPortrait;
bool blockUnknownConnections = false;
bool streamerMode = false;
/// Set the dark theme.
void setDark() {
@ -74,11 +78,11 @@ class Settings extends ChangeNotifier {
// 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"]);
@ -105,6 +109,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).
@ -227,6 +236,7 @@ class Settings extends ChangeNotifier {
"Theme": themeString,
"PreviousPid": -1,
"BlockUnknownConnections": blockUnknownConnections,
"StreamerMode": streamerMode,
"ExperimentsEnabled": this.experimentsEnabled,
"Experiments": experiments,
"StateRootPane": 0,

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';
@ -197,7 +197,7 @@ 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.map<DropdownMenuItem<String>>((RemoteServerInfoState serverInfo) {
return DropdownMenuItem<String>(
value: serverInfo.onion,
child: Text(
@ -240,8 +240,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,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/model.dart';
@ -106,6 +107,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
labelText: AppLocalizations.of(context)!.yourDisplayName,
validator: (value) {
if (value.isEmpty) {
// TODO l10n ize
return "Please enter a display name";
}
return null;
@ -263,7 +265,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),
))
@ -288,32 +289,19 @@ 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<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);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, updateNameEventJson);
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
final updatePasswordEvent = {
"EventType": "ChangePassword",
"Data": {"Password": ctrlrOldPass.text, "NewPassword": ctrlrPass.text}
@ -330,13 +318,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,398 @@
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;
//late bool deleted;
@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, 0, 30, 10),
padding: EdgeInsets.fromLTRB(20, 0 , 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: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress),
SizedBox(
height: 20,
),
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,
labelText: "Description",
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.defaultButtonActiveColor(),
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.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor()),
),
// ***** 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

@ -4,7 +4,7 @@ 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';
@ -23,18 +23,20 @@ class ContactsView extends StatefulWidget {
// selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, String handle) {
var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
// 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, initialIndex);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
}
void _pushMessageView(BuildContext context, String handle, int initialIndex) {
void _pushMessageView(BuildContext context, String handle) {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push(
MaterialPageRoute<void>(
@ -47,7 +49,7 @@ void _pushMessageView(BuildContext context, String handle, int initialIndex) {
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
],
builder: (context, child) => MessageView(initialIndex),
builder: (context, child) => MessageView(),
);
},
),
@ -86,18 +88,7 @@ class _ContactsViewState extends State<ContactsView> {
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()))),
])),
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,
@ -107,6 +98,36 @@ 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));
}));
// TODO servers
// 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,

View File

@ -17,7 +17,6 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
Widget build(BuildContext context) {
var flwtch = Provider.of<AppState>(context);
var cols = Provider.of<Settings>(context).uiColumns(true);
var initialIndex = flwtch.selectedConversation == null ? 0 : Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(flwtch.selectedConversation!)!.unreadMessages;
return Flex(
direction: Axis.horizontal,
children: <Widget>[
@ -36,7 +35,7 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
ChangeNotifierProvider.value(
value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", "")),
], child: Container(child: MessageView(initialIndex))),
], child: Container(key: Key(flwtch.selectedConversation??"never_this"), child: MessageView())),
),
],
);

View File

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/servers.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
@ -137,6 +139,19 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.block_unknown, color: settings.current().mainTextColor()),
),
SwitchListTile(
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.defaultButtonActiveColor(),
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),
@ -175,6 +190,59 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
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.defaultButtonActiveColor(),
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.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()),
),
SwitchListTile(
title: Text("Enable Clickable Links", style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text("The clickable links experiment allows you to click on URLs shared in messages."),
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(
@ -227,6 +295,9 @@ String getLanguageFull(context, String languageCode) {
if (languageCode == "pl") {
return AppLocalizations.of(context)!.localePl;
}
if (languageCode == "ru") {
return AppLocalizations.of(context)!.localeRU;
}
return languageCode;
}

View File

@ -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).onion;
// 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,20 +184,21 @@ 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);
// 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

@ -6,6 +6,9 @@ import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/quotedmessage.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';
@ -14,6 +17,7 @@ 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';
@ -22,9 +26,6 @@ import '../widgets/messagelist.dart';
import 'groupsettingsview.dart';
class MessageView extends StatefulWidget {
int initialIndex;
MessageView(this.initialIndex);
@override
_MessageViewState createState() => _MessageViewState();
}
@ -38,24 +39,30 @@ class _MessageViewState extends State<MessageView> {
@override
void initState() {
// using "8" because "# of messages that fit on one screen" isnt trivial to calculate at this point
if (widget.initialIndex > 8) {
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((timeStamp) {
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = true;
});
}
scrollListener.itemPositions.addListener(() {
var first = scrollListener.itemPositions.value.first.index;
var last = scrollListener.itemPositions.value.last.index;
// sometimes these go hi->lo and sometimes they go lo->hi because [who tf knows]
if (first == 0 || last == 0) {
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
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() {
focusNode.dispose();
@ -70,13 +77,41 @@ class _MessageViewState extends State<MessageView> {
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,
));
}
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(
floatingActionButton: appState.unreadMessagesBelow ? FloatingActionButton(child: Icon(Icons.arrow_downward), onPressed: (){
scrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
}) : null,
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,
@ -97,17 +132,9 @@ class _MessageViewState extends State<MessageView> {
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(widget.initialIndex, scrollController, scrollListener)),
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(scrollController, scrollListener)),
bottomSheet: _buildComposeBox(),
));
}
@ -175,11 +202,19 @@ class _MessageViewState extends State<MessageView> {
_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).onion, filePath);
_sendMessageHelper();
}
void _sendMessageHelper() {
ctrlrCompose.clear();
focusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 80), () {
Provider.of<ContactInfoState>(context, listen: false).totalMessages++;
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());
});
@ -196,42 +231,36 @@ class _MessageViewState extends State<MessageView> {
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,
enabled: !isOffline,
decoration: InputDecoration(
hintText: isOffline ? "" : AppLocalizations.of(context)!.placeholderEnterMessage,
hintStyle: TextStyle(color: Provider.of<Settings>(context).theme.altTextColor()),
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: isOffline ? null : _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.altTextColor()),
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,
))),
)))),
],
),
);
@ -249,18 +278,27 @@ class _MessageViewState extends State<MessageView> {
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;
},
))
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();
@ -334,4 +372,20 @@ class _MessageViewState extends State<MessageView> {
));
});
}
void _showFilePicker() async {
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);
_sendFile(file.path);
} else {
print("file size cannot exceed 10 gigabytes");
//todo: toast error
}
}
}
}

View File

@ -196,13 +196,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).onion;
// 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),
))
])
]),
@ -226,13 +234,15 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
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);
// 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();
@ -51,16 +52,19 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
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,
@ -94,6 +98,11 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
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));
@ -118,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) {
@ -129,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(
@ -215,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,
));
}

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

@ -0,0 +1,152 @@
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(
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

@ -35,7 +35,7 @@ class _TripleColumnViewState extends State<TripleColumnView> {
child: appState.selectedConversation == null
? Center(child: Text(AppLocalizations.of(context)!.addContactFirst))
: //dev
Container(child: MessageView(0/*todo:setme*/)),
Container(child: MessageView()),
),
]);
}

View File

@ -37,6 +37,7 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
readOnly: widget.readonly,
showCursor: !widget.readonly,
focusNode: _focusNode,
enableIMEPersonalizedLearning: false,
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: widget.onPressed,

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
@ -63,8 +65,11 @@ class _ContactRowState extends State<ContactRow> {
child: LinearProgressIndicator(
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
)),
Text(contact.onion,
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor() : Provider.of<Settings>(context).theme.mainTextColor())),
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(
@ -127,9 +132,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());
}
}

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

@ -0,0 +1,292 @@
import 'dart:io';
import 'package:cwtch/models/message.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;
FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.interactive = true});
@override
FileBubbleState createState() => FileBubbleState();
String fileKey() {
return this.rootHash + "." + this.nonce;
}
}
class FileBubbleState extends State<FileBubble> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var flagStarted = Provider.of<MessageMetadata>(context).flags & 0x02 > 0;
var borderRadiousEh = 15.0;
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
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);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
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())));
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 = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
} else if (Provider.of<ProfileInfoState>(context).downloadComplete(widget.fileKey())) {
// in this case, whatever marked download.complete would have also set the path
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey())!;
wdgDecorations = Text(AppLocalizations.of(context)!.fileSavedTo + ': ' + path + '\u202F');
} else if (Provider.of<ProfileInfoState>(context).downloadActive(widget.fileKey())) {
if (!Provider.of<ProfileInfoState>(context).downloadGotManifest(widget.fileKey())) {
wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F');
} else {
wdgDecorations = 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 (!Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey()) ) {
wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F');
Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey());
} else {
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()) ?? "";
wdgDecorations = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:[Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton))]
);
}
} else {
wdgDecorations = Center(
widthFactor: 1,
child: Wrap(children: [
Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.downloadFileButton + '\u202F'), onPressed: _btnAccept)),
]));
}
return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString());
return Center(
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),
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: Center(
widthFactor: 1.0,
child: Padding(
padding: EdgeInsets.all(9.0),
child: Wrap(alignment: WrapAlignment.start, children: [
Center(
widthFactor: 1.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, wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)]),
)
])))));
});
}
void _btnAccept() async {
String? selectedFileName;
File? file;
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var handle = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
if (Platform.isAndroid) {
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02);
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey());
} else {
try {
selectedFileName = await saveFile(
defaultFileName: widget.nameSuggestion,
);
if (selectedFileName != null) {
file = File(selectedFileName);
print("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.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02);
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, handle, 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).senderHandle;
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,
)),
);
}
}

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';
@ -39,7 +40,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
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);
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 = "";
@ -84,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)),
]));
}
@ -130,7 +131,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
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;
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01;
});
}

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,9 +32,10 @@ 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 = "";
@ -43,16 +50,40 @@ 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()));
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),
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);
@ -88,4 +119,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);
} else {
throw 'Could not launch $link';
}
},
),
),
]),
],
)),
));
});
}
}

View File

@ -9,10 +9,9 @@ import '../model.dart';
import '../settings.dart';
class MessageList extends StatefulWidget {
int initialIndex;
ItemScrollController scrollController;
ItemPositionsListener scrollListener;
MessageList(this.initialIndex, this.scrollController, this.scrollListener);
MessageList(this.scrollController, this.scrollListener);
@override
_MessageListState createState() => _MessageListState();
@ -21,6 +20,7 @@ class MessageList extends StatefulWidget {
class _MessageListState extends State<MessageList> {
@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";
@ -56,45 +56,44 @@ class _MessageListState extends State<MessageList> {
Text("")),
))),
Expanded(
child: Scrollbar(
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
? ScrollablePositionedList.builder(
itemPositionsListener: widget.scrollListener,
itemScrollController: widget.scrollController,
initialScrollIndex: widget.initialIndex,
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;
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
? 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).onion;
var messageIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
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;
// Already includes MessageRow,,
return message.getWidget(context);
} else {
return Text(''); //MessageLoadingBubble();
}
},
)
: null)))
);
},
)
: null))
])));
}
}

View File

@ -5,6 +5,7 @@ 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';
@ -20,9 +21,30 @@ class MessageRow extends StatefulWidget {
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;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
@ -30,6 +52,12 @@ class MessageRowState extends State<MessageRow> {
var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false;
var actualMessage = Flexible(flex: 3, fit: FlexFit.loose, child: widget.child);
_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.getContact(Provider.of<MessageMetadata>(context).senderHandle);
@ -41,7 +69,7 @@ class MessageRowState extends State<MessageRow> {
}
Widget wdgIcons = Visibility(
visible: this.showMenu,
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageIndex,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
@ -52,7 +80,7 @@ class MessageRowState extends State<MessageRow> {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
},
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
Widget wdgSpacer = Flexible(child: SizedBox(width: 60, height: 10));
var widgetRow = <Widget>[];
if (fromMe) {
@ -94,7 +122,6 @@ class MessageRowState extends State<MessageRow> {
child: TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(blockedMessageBackground),
overlayColor: MaterialStateProperty.all(blockedMessageBackground),
),
child: Text(
AppLocalizations.of(context)!.showMessageButton + '\u202F',
@ -131,29 +158,94 @@ class MessageRowState extends State<MessageRow> {
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).messageIndex;
});
},
onExit: (event) {
setState(() {
this.showMenu = false;
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
});
},
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_dragAlignment += Alignment(
details.delta.dx / (size.width * 0.5),
0,
);
});
},
onPanDown: (details) {
_controller.stop();
},
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
},
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 && mark == Provider.of<ContactInfoState>(context).totalMessages - Provider.of<MessageMetadata>(context).messageIndex) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Align(alignment:Alignment.center ,child:_bubbleNew()), mr]);
} else {
return mr;
}
}
// Swipe to quote on Android
onHorizontalDragEnd: Platform.isAndroid
? (details) {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
}
: null,
child: Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow))));
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() {
@ -173,14 +265,14 @@ class MessageRowState extends State<MessageRow> {
showAddContactConfirmAlertDialog(BuildContext context, String profileOnion, String senderOnion) {
// 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)!.addContact),
onPressed: () {

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,

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';
@ -46,12 +47,14 @@ 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(
@ -59,7 +62,7 @@ class _ProfileRowState extends State<ProfileRow> {
tooltip: AppLocalizations.of(context)!.editProfile + " " + profile.nickname,
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);
},
)
],
@ -98,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).reset();
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(

View File

@ -0,0 +1,97 @@
import 'package:cwtch/main.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/views/addeditservers.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../cwtch_icons_icons.dart';
import '../errorHandler.dart';
import '../model.dart';
import '../settings.dart';
class ServerRow extends StatefulWidget {
@override
_ServerRowState createState() => _ServerRowState();
}
class _ServerRowState extends State<ServerRow> {
@override
Widget build(BuildContext context) {
var server = Provider.of<ServerInfoState>(context);
return Card(clipBehavior: Clip.antiAlias,
margin: EdgeInsets.all(0.0),
child: InkWell(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(6.0), //border size
child: Icon(CwtchIcons.dns_24px,
color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
size: 64)
),
Expanded(
child: Column(
children: [
Text(
server.description,
semanticsLabel: server.description,
style: Provider.of<FlwtchState>(context).biggerFont.apply(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: ExcludeSemantics(
child: Text(
server.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
)))
],
)),
// Copy server button
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context)!.copyServerKeys,
icon: Icon(CwtchIcons.address_copy_2, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () {
Clipboard.setData(new ClipboardData(text: server.serverBundle));
},
),
// Edit button
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context)!.editServerTitle,
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () {
_pushEditServer(server);
},
)
])));
}
void _pushEditServer(ServerInfoState server ) {
Provider.of<ErrorHandler>(context).reset();
Navigator.of(context).push(MaterialPageRoute<void>(
settings: RouteSettings(name: "serveraddedit"),
builder: (BuildContext context) {
return MultiProvider(
providers: [ChangeNotifierProvider<ServerInfoState>(
create: (_) => server,
)],
child: AddEditServerView(),
);
},
));
}
}

View File

@ -39,6 +39,7 @@ class _CwtchTextFieldState extends State<CwtchTextField> {
validator: widget.validator,
onChanged: widget.onChanged,
autofocus: widget.autofocus,
enableIMEPersonalizedLearning: false,
focusNode: _focusNode,
decoration: InputDecoration(
labelText: widget.labelText,

View File

@ -1,3 +1,3 @@
#!/bin/sh
env LD_LIBRARY_PATH=./lib/ ./lib/cwtch
exec env LD_LIBRARY_PATH=./lib/ ./lib/cwtch

View File

@ -1,3 +1,3 @@
#!/bin/sh
env LD_LIBRARY_PATH=~/.local/lib/cwtch/ ~/.local/lib/cwtch/cwtch
exec env LD_LIBRARY_PATH=~/.local/lib/cwtch/ ~/.local/lib/cwtch/cwtch

View File

@ -1,3 +1,3 @@
#!/bin/sh
env LD_LIBRARY_PATH=/usr/lib/cwtch /usr/lib/cwtch/cwtch
exec env LD_LIBRARY_PATH=/usr/lib/cwtch /usr/lib/cwtch/cwtch

View File

@ -2,6 +2,7 @@
mkdir -p ~/.local/bin
sed "s|~|$HOME|g" cwtch.home.sh > ~/.local/bin/cwtch
chmod a+x ~/.local/bin/cwtch
mkdir -p ~/.local/share/icons
cp cwtch.png ~/.local/share/icons
@ -13,4 +14,5 @@ mkdir -p ~/.local/lib/cwtch
cp -r lib/* ~/.local/lib/cwtch
mkdir -p ~/.local/share/applications
sed "s|~|$HOME|g" cwtch.home.desktop > $HOME/.local/share/applications/cwtch.desktop
sed "s|~|$HOME|g" cwtch.home.desktop > $HOME/.local/share/applications/cwtch.desktop
chmod a+x $HOME/.local/share/applications/cwtch.desktop

View File

@ -1,6 +1,7 @@
#!/bin/sh
cp cwtch.sys.sh /usr/bin/cwtch
chmod a+x /usr/bin/cwtch
cp cwtch.png /usr/share/icons
@ -11,3 +12,4 @@ mkdir -p /usr/lib/cwtch
cp -r lib/* /usr/lib/cwtch
cp cwtch.sys.desktop /usr/share/applications/cwtch.desktop
chmod a+x /usr/share/applications/cwtch.desktop

6
macos/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/xcuserdata/

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -0,0 +1,16 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import package_info_plus_macos
import path_provider_macos
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

40
macos/Podfile Normal file
View File

@ -0,0 +1,40 @@
platform :osx, '10.11'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

34
macos/Podfile.lock Normal file
View File

@ -0,0 +1,34 @@
PODS:
- FlutterMacOS (1.0.0)
- package_info_plus_macos (0.0.1):
- FlutterMacOS
- path_provider_macos (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
package_info_plus_macos:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
path_provider_macos:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
COCOAPODS: 1.11.2

View File

@ -0,0 +1,634 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
211091843422DC99794C0E66 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C147BFC49BDAD7E14E179AF3 /* Pods_Runner.framework */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1AF219FB7E04D0D2DBC075A5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
2FFAA895D8F20891DA4D87C5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* Cwtch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cwtch.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
5EEB7EA2235BC5CDA2BCB6A9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
C147BFC49BDAD7E14E179AF3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
211091843422DC99794C0E66 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
35B90E5140F9C2DE6D3BD07E /* Pods */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* Cwtch.app */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
35B90E5140F9C2DE6D3BD07E /* Pods */ = {
isa = PBXGroup;
children = (
2FFAA895D8F20891DA4D87C5 /* Pods-Runner.debug.xcconfig */,
1AF219FB7E04D0D2DBC075A5 /* Pods-Runner.release.xcconfig */,
5EEB7EA2235BC5CDA2BCB6A9 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
C147BFC49BDAD7E14E179AF3 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
F13BA066A536BB902BFE0B8C /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
73F636226F48A847E9232926 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* Cwtch.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0930;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
73F636226F48A847E9232926 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
F13BA066A536BB902BFE0B8C /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_NAME = Cwtch;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_NAME = Cwtch;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_NAME = Cwtch;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Cwtch.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Cwtch.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Cwtch.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Cwtch.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,9 @@
import Cocoa
import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}

View File

@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,339 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>

View File

@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = ui
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = im.cwtch.ui
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2021 Open Privacy Research Society. All rights reserved.

View File

@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

View File

@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

View File

@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

32
macos/Runner/Info.plist Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

BIN
macos/cwtch.icns Normal file

Binary file not shown.

25
macos/make-icns.sh Executable file
View File

@ -0,0 +1,25 @@
input_filepath="../cwtch.png"
output_iconset_name="cwtch.iconset"
mkdir $output_iconset_name
sips -z 16 16 $input_filepath --out "${output_iconset_name}/icon_16x16.png"
cp "${output_iconset_name}/icon_16x16.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
sips -z 32 32 $input_filepath --out "${output_iconset_name}/icon_16x16@2x.png"
sips -z 32 32 $input_filepath --out "${output_iconset_name}/icon_32x32.png"
cp "${output_iconset_name}/icon_32x32.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
sips -z 64 64 $input_filepath --out "${output_iconset_name}/icon_32x32@2x.png"
cp "${output_iconset_name}/icon_32x32@2x.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
sips -z 128 128 $input_filepath --out "${output_iconset_name}/icon_128x128.png"
cp "${output_iconset_name}/icon_128x128.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
sips -z 256 256 $input_filepath --out "${output_iconset_name}/icon_128x128@2x.png"
sips -z 256 256 $input_filepath --out "${output_iconset_name}/icon_256x256.png"
cp "${output_iconset_name}/icon_256x256.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
sips -z 512 512 $input_filepath --out "${output_iconset_name}/icon_256x256@2x.png"
sips -z 512 512 $input_filepath --out "${output_iconset_name}/icon_512x512.png"
cp "${output_iconset_name}/icon_512x512.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
sips -z 1024 1024 $input_filepath --out "${output_iconset_name}/icon_1024x1024.png"
cp "${output_iconset_name}/icon_1024x1024.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
iconutil -c icns $output_iconset_name
rm -R $output_iconset_name

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