Compare commits

...

191 Commits
aar64 ... trunk

Author SHA1 Message Date
Dan Ballard bfae3b577f final fixes for windows package
continuous-integration/drone/push Build is passing Details
2021-06-30 09:46:22 -07:00
Dan Ballard b20a82fb1a nsis installer now tested on windows
continuous-integration/drone/push Build is failing Details
2021-06-29 19:39:24 -07:00
Dan Ballard 302e504d76 add LICENSE for build steps nsis
continuous-integration/drone/push Build is failing Details
2021-06-29 19:17:03 -07:00
Dan Ballard 00c77c97d2 updated nsis installer script
continuous-integration/drone/push Build is failing Details
2021-06-29 19:07:19 -07:00
Dan Ballard d12efa2204 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into trunk
continuous-integration/drone/push Build is failing Details
2021-06-29 17:26:04 -07:00
Dan Ballard ee10637261 drone test nsis installer 2021-06-29 16:20:54 -07:00
Sarah Jamie Lewis 3cf87e825b Update 'README.md'
continuous-integration/drone/push Build is passing Details
2021-06-25 07:38:05 -07:00
Dan Ballard 96d9090b1d wow Get-FileHash is a truely garbage cmdlet
continuous-integration/drone/push Build is passing Details
2021-06-25 00:56:24 -07:00
Dan Ballard ca58e063b6 guess i m tired and dumb
continuous-integration/drone/push Build is failing Details
2021-06-25 00:45:14 -07:00
Dan Ballard 2d537df810 signtool syntax dumbness
continuous-integration/drone/push Build is failing Details
2021-06-25 00:35:09 -07:00
Dan Ballard 94117e88d2 signtool dumb path
continuous-integration/drone/push Build is failing Details
2021-06-25 00:24:42 -07:00
Dan Ballard 034a856c05 and so the journy of variables being syntax errors begins
continuous-integration/drone/push Build is failing Details
2021-06-24 22:39:29 -07:00
Dan Ballard 69c748dcc4 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into trunk
continuous-integration/drone/push Build is failing Details
2021-06-24 22:34:24 -07:00
Dan Ballard 9ae38f6870 sign the exe 2021-06-24 22:34:17 -07:00
Dan Ballard 97c4deb4bf Merge pull request 'Remove todos + show/hide password translations' (#218) from launch into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #218
2021-06-24 14:48:15 -07:00
Sarah Jamie Lewis 8d6d5a23b9 Merge branch 'trunk' into launch
continuous-integration/drone/pr Build is passing Details
2021-06-24 14:38:33 -07:00
Sarah Jamie Lewis 9a6dbae0c4 Remove todos + show/hide password translations
continuous-integration/drone/pr Build is passing Details
2021-06-24 14:37:19 -07:00
erinn 6eccece360 Merge pull request 'Add Syncing Warning for Groups' (#217) from launch into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #217
2021-06-24 14:05:43 -07:00
Sarah Jamie Lewis d538d8442e Merge branch 'trunk' into launch
continuous-integration/drone/pr Build is passing Details
2021-06-24 14:05:03 -07:00
Sarah Jamie Lewis 62ef3549b5 Add Syncing Warning for Groups
continuous-integration/drone/pr Build is passing Details
2021-06-24 13:38:41 -07:00
Dan Ballard e33a543e66 Merge pull request 'Add Flaticons Licenses, Fix Splash Screen, Fix Android License Prompt' (#216) from launch into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #216
2021-06-24 13:07:20 -07:00
Sarah Jamie Lewis 0090e86ade Add Flaticons Licenses, Fix Splash Screen, Fix Android License Prompt
continuous-integration/drone/pr Build is passing Details
2021-06-24 12:57:48 -07:00
Dan Ballard 82fe74937c pubspec bump
continuous-integration/drone/push Build is passing Details
2021-06-24 12:20:05 -07:00
Dan Ballard 23b0af77f1 Merge pull request 'Experimental Check on Group Invitations + Delete Profile Fixes for Passwordless Profiles' (#214) from launch into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #214
2021-06-24 12:07:50 -07:00
Sarah Jamie Lewis dad995cd3c Add Contact Header Image + Contact Selection
continuous-integration/drone/pr Build is passing Details
2021-06-24 11:48:18 -07:00
Sarah Jamie Lewis 17518b1e6d Bump libcwtch-go version
continuous-integration/drone/pr Build is passing Details
2021-06-24 11:26:16 -07:00
Sarah Jamie Lewis 0c80577f72 Hide "Current Password" field for password-less accounts 2021-06-24 11:26:16 -07:00
Sarah Jamie Lewis a47a17b3a9 Experimental Check on Group Invitations 2021-06-24 11:26:16 -07:00
Dan Ballard 5ca0712d5e fix tests, rename themes from cwtch* to opaque* 2021-06-24 11:26:16 -07:00
Dan Ballard b61f59643f contacts that are blocked get blocked colors; rework opaque theming a little, start using for font size 2021-06-24 11:26:14 -07:00
erinn 82ee06d840 dual pane wiring 2021-06-24 11:24:50 -07:00
erinn eb12f57135 dualpane settings wiring 2021-06-24 11:24:50 -07:00
Sarah Jamie Lewis ddb671cf6a Merge pull request 'contacts that are blocked get blocked colors; rework opaque theming a little, start using for font size' (#200) from opaqueBlock into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #200
2021-06-24 11:20:48 -07:00
Sarah Jamie Lewis 93dbba5f15 Merge branch 'trunk' into opaqueBlock
continuous-integration/drone/pr Build is passing Details
2021-06-24 11:20:34 -07:00
Dan Ballard 1c961c2c4c fix tests, rename themes from cwtch* to opaque*
continuous-integration/drone/pr Build is passing Details
2021-06-24 10:59:23 -07:00
Dan Ballard 040bb585a1 drone windows cmd/ps syntax is corrosive to brains
continuous-integration/drone/push Build is passing Details
2021-06-24 01:31:41 -07:00
Dan Ballard e89c4bdb0e drone windows cmd/ps syntax is corrosive to brains
continuous-integration/drone/push Build is failing Details
2021-06-24 01:21:27 -07:00
Dan Ballard f8e5bd29f3 drone windows cmd/ps syntax is toxic to brains
continuous-integration/drone/push Build was killed Details
2021-06-24 01:15:35 -07:00
Dan Ballard a18a522f81 drone windows beyond terrible
continuous-integration/drone/push Build is failing Details
2021-06-24 01:04:34 -07:00
Dan Ballard 16ce38da75 drone windows hyper dumb
continuous-integration/drone/push Build is failing Details
2021-06-24 00:51:06 -07:00
Dan Ballard 126c68e185 drone windows hyper dumb
continuous-integration/drone/push Build is failing Details
2021-06-24 00:40:16 -07:00
Dan Ballard c533580b70 drone windows hyper dumb
continuous-integration/drone/push Build is failing Details
2021-06-24 00:26:43 -07:00
Dan Ballard ca303ba41c drone windows hyper dumb
continuous-integration/drone/push Build is failing Details
2021-06-24 00:13:27 -07:00
Dan Ballard ff4a8a02ab drone windows hyper dumb
continuous-integration/drone/push Build is failing Details
2021-06-24 00:01:29 -07:00
Dan Ballard 6c0c31a74f drone windows is dumb
continuous-integration/drone/push Build is failing Details
2021-06-23 23:50:55 -07:00
Dan Ballard 4e0be20930 drone windows is dumb
continuous-integration/drone/push Build is failing Details
2021-06-23 23:27:35 -07:00
Dan Ballard 0a93dd40b6 drone windows compress archive is dumb
continuous-integration/drone/push Build is failing Details
2021-06-23 23:16:35 -07:00
Dan Ballard 5f6008a152 drone windows msix debug
continuous-integration/drone/push Build is failing Details
2021-06-23 23:03:22 -07:00
Dan Ballard 901293377e drone windows msix debug
continuous-integration/drone/push Build is failing Details
2021-06-23 22:41:05 -07:00
Dan Ballard a54d50a642 drone window: rework order, variables, so dlls and tor are in msix; remove pub pligin window_size
continuous-integration/drone/push Build is failing Details
2021-06-23 22:18:31 -07:00
Dan Ballard f7c30755d0 remove pub pligin window_size
continuous-integration/drone/push Build is passing Details
2021-06-23 21:15:35 -07:00
Dan Ballard 66568a7983 drone windows msix do pubspec.yaml sub before flutter get
continuous-integration/drone/push Build is failing Details
2021-06-23 18:47:52 -07:00
Dan Ballard da8a35458f drone windows msix powershell Set-Content for utf not >
continuous-integration/drone/push Build is failing Details
2021-06-23 18:35:36 -07:00
Dan Ballard ae2ae51d2d drone windows msix powershell cant read and pipe write to the same file...
continuous-integration/drone/push Build is failing Details
2021-06-23 18:10:52 -07:00
Dan Ballard b7a4486536 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into trunk
continuous-integration/drone/push Build is failing Details
2021-06-23 17:57:01 -07:00
Dan Ballard 3e56f8c917 drone windows msix 2021-06-23 17:56:56 -07:00
erinn c4203477de Merge pull request 'Fix Reconnect Woes + DeleteProfile on Android' (#206) from beta_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #206
2021-06-23 15:02:47 -07:00
erinn 1c1a1ec68d Merge branch 'trunk' into beta_fixes
continuous-integration/drone/pr Build is passing Details
2021-06-23 15:02:37 -07:00
Dan Ballard 225301bda6 drone ./fetch
continuous-integration/drone/push Build is passing Details
2021-06-23 13:55:44 -07:00
Sarah Jamie Lewis 47394f5183 Merge pull request 'update tor versions and fetching of them' (#212) from torUp into trunk
continuous-integration/drone/push Build was killed Details
Reviewed-on: #212
2021-06-23 13:52:10 -07:00
Sarah Jamie Lewis 0c0a8501c9 Merge branch 'trunk' into torUp
continuous-integration/drone/pr Build is failing Details
2021-06-23 13:51:33 -07:00
Dan Ballard 7268fc362f add mkdir -p to fetch-tor.sh
continuous-integration/drone/pr Build is passing Details
2021-06-23 13:43:58 -07:00
Dan Ballard e0d1e1cb2b drone: change tor fetching in prep for PR
continuous-integration/drone/push Build is failing Details
2021-06-23 13:32:35 -07:00
Dan Ballard f0e0af3537 update tor versions and fetching of them
continuous-integration/drone/pr Build is passing Details
2021-06-23 13:31:48 -07:00
Sarah Jamie Lewis e266d6328a Merge branch 'trunk' into beta_fixes
continuous-integration/drone/pr Build is passing Details
2021-06-22 17:49:18 -07:00
Sarah Jamie Lewis 94014ede01 bump version
continuous-integration/drone/pr Build is passing Details
2021-06-22 17:49:05 -07:00
Dan Ballard 547c2de725 windows tor version bump to 0.4.5.9
continuous-integration/drone/push Build is passing Details
2021-06-22 17:16:21 -07:00
Sarah Jamie Lewis 066c1965e6 Fix Reconnect Woes
continuous-integration/drone/pr Build is passing Details
2021-06-22 17:05:38 -07:00
Sarah Jamie Lewis ee2fd2ae98 Fix DeleteProfile in Android
continuous-integration/drone/pr Build is passing Details
2021-06-22 12:44:36 -07:00
Dan Ballard 607802d4e4 Merge pull request 'Bump Version' (#204) from beta_fixes into trunk
continuous-integration/drone/push Build is failing Details
Reviewed-on: #204
2021-06-21 22:14:14 -07:00
Sarah Jamie Lewis ecd1b71a8c Bump Version
continuous-integration/drone/pr Build is passing Details
2021-06-21 20:37:11 -07:00
Sarah Jamie Lewis 4e63694ebc Merge pull request 'android process handling improvements' (#203) from countersync into trunk
continuous-integration/drone/push Build is failing Details
Reviewed-on: #203
2021-06-21 18:08:27 -07:00
erinn a7fe5f0f80 bump lcg
continuous-integration/drone/pr Build is passing Details
2021-06-21 17:58:46 -07:00
erinn 4b67879f64 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into countersync
continuous-integration/drone/pr Build is passing Details
2021-06-21 17:56:41 -07:00
erinn d0ba17b0e9 android process handling improvements 2021-06-21 17:56:34 -07:00
Dan Ballard c5154a91d1 contacts that are blocked get blocked colors; rework opaque theming a little, start using for font size
continuous-integration/drone/pr Build is failing Details
2021-06-18 20:31:25 -07:00
erinn 886e0956f6 improve broadcast receiver antiduplication 2021-06-18 18:17:22 -07:00
Sarah Jamie Lewis 4671a15c27 Merge pull request 'countersync' (#198) from countersync into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #198
2021-06-17 16:19:04 -07:00
erinn a03a665a66 remove deprecated methods
continuous-integration/drone/pr Build is passing Details
2021-06-17 16:03:54 -07:00
erinn b85f1464d3 bump lcg
continuous-integration/drone/pr Build is failing Details
2021-06-17 15:51:40 -07:00
erinn 09d9771845 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into countersync 2021-06-17 15:44:47 -07:00
erinn 24139a4683 counter sync 2021-06-17 15:44:42 -07:00
Dan Ballard 398d7a286a drone syntax fix
continuous-integration/drone/push Build is passing Details
2021-06-17 14:28:22 -07:00
Dan Ballard bb739027ea Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into trunk 2021-06-17 14:26:49 -07:00
Dan Ballard 506ddeeb10 drone new stage test-android-build does one apk --debug build 2021-06-17 14:26:39 -07:00
Sarah Jamie Lewis 219cd3889a Merge pull request 'use QueryACNVersion api to populate and display tor version on tor screen' (#164) from torVer into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #164
2021-06-17 14:22:50 -07:00
Dan Ballard d3a6ec85cf Handle AVNVersion message and display version on Tor screen
continuous-integration/drone/pr Build is passing Details
2021-06-17 14:18:22 -07:00
erinn 8b8b20e7b6 Merge pull request 'Fix Server Import' (#197) from beta_fixes into trunk
continuous-integration/drone/push Build is failing Details
Reviewed-on: #197
2021-06-17 14:01:24 -07:00
Sarah Jamie Lewis 0cbc4298e2 Fix Server Import
continuous-integration/drone/pr Build is passing Details
2021-06-17 13:59:24 -07:00
erinn 4377118629 Merge pull request 'add group message notifications' (#195) from groupnotifs into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #195
2021-06-17 13:52:16 -07:00
erinn 2d49c50de5 bump lcg
continuous-integration/drone/pr Build is passing Details
2021-06-17 13:34:39 -07:00
erinn 4d3777be81 bump lcg
continuous-integration/drone/pr Build is failing Details
2021-06-17 13:31:36 -07:00
erinn 88c470016b Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into groupnotifs 2021-06-17 13:30:56 -07:00
Dan Ballard a10da52767 drone linux: include new desktop files and install scripts
continuous-integration/drone/push Build is passing Details
2021-06-17 12:27:24 -07:00
Sarah Jamie Lewis 4b6f022a92 Merge pull request '.desktop files and installer scripts for linux and fixes for loading in different configs' (#196) from linuxInstall into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #196
2021-06-16 19:14:53 -07:00
Dan Ballard 92577aef07 .desktop files and installer scripts for linux and fixes for loading in different configs
continuous-integration/drone/pr Build is passing Details
2021-06-16 18:01:31 -07:00
erinn cc4ec567fb m
continuous-integration/drone/pr Build is failing Details
2021-06-16 16:53:18 -07:00
erinn 0b7f2b13bd notification onclick to group not peer 2021-06-16 16:51:16 -07:00
erinn 24854c24f5 adding group message notifications 2021-06-16 16:45:37 -07:00
erinn 564556d8fa merge trunk 2021-06-16 16:06:52 -07:00
erinn ed621e7795 adding group message notifications 2021-06-16 16:03:39 -07:00
erinn 30ae6ca331 Merge pull request 'Shutdown Cwtch' (#194) from beta_fixes into trunk
continuous-integration/drone/push Build is failing Details
Reviewed-on: #194
2021-06-16 15:36:59 -07:00
Sarah Jamie Lewis 5b96d1c794 Format
continuous-integration/drone/pr Build is failing Details
2021-06-16 15:33:09 -07:00
Sarah Jamie Lewis 87732a2ba2 Allow shutdown to be called by method handler
continuous-integration/drone/pr Build is passing Details
2021-06-16 15:30:18 -07:00
Sarah Jamie Lewis 213a29c691 update libcwtch-go 2021-06-16 15:26:31 -07:00
Sarah Jamie Lewis 6b93f059d7 Fix Invite Handling 2021-06-16 15:26:02 -07:00
Sarah Jamie Lewis 052649ef9d Shutdown Cwtch 2021-06-16 15:26:02 -07:00
erinn 7940c27709 Merge pull request 'new splash flow in conjunction with startCwtch changes in libcwtch-go; new version of flutter splash with error message display ability; cleaning old code' (#193) from splashPt1 into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #193
2021-06-16 15:23:05 -07:00
Dan Ballard 1888d3f032 new animated knot android splash; assets for native flutter splash
continuous-integration/drone/pr Build is passing Details
2021-06-16 13:30:39 -07:00
Dan Ballard 650148d731 new splash flow in conjunction with startCwtch changes in libcwtch-go; new version of flutter splash with error message display ability; cleaning old code
continuous-integration/drone/pr Build is passing Details
2021-06-15 13:10:07 -07:00
Dan Ballard f08062c0fb Merge pull request 'Delete Profile / Leave Conversation + Translation Dump via Erinn' (#191) from beta_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #191
2021-06-15 13:07:28 -07:00
Sarah Jamie Lewis e55a7e940e Fix autofocus on android + auto select text fields in settings to make them easier to change.
continuous-integration/drone/pr Build is passing Details
2021-06-15 12:44:50 -07:00
Sarah Jamie Lewis 1e2c69eb4c Translations 2021-06-15 12:44:50 -07:00
Sarah Jamie Lewis 2a3945639d Delete Profile / Leave Conversation + Android Fixes 2021-06-15 12:44:50 -07:00
Sarah Jamie Lewis 16f9928275 Merge pull request 'linux assetsDir support linux style installs' (#180) from assetsDir into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #180
2021-06-14 17:41:34 -07:00
Sarah Jamie Lewis 07e8c49d7b Merge branch 'trunk' into assetsDir
continuous-integration/drone/pr Build is passing Details
2021-06-14 17:41:26 -07:00
Sarah Jamie Lewis 38c59c5f82 Merge pull request 'l10n updates' (#190) from l10nupdates into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #190
2021-06-14 17:28:08 -07:00
erinn 5681a43d69 l10n updates
continuous-integration/drone/pr Build is passing Details
2021-06-14 17:25:24 -07:00
erinn c3e3c4f91e l10n updates
continuous-integration/drone/pr Build is passing Details
2021-06-14 17:11:43 -07:00
Dan Ballard 43bf1c19ae Merge pull request 'Remove debug button in release builds' (#189) from beta_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #189
2021-06-14 14:56:47 -07:00
Sarah Jamie Lewis 28603b6fed Remove debug button in release builds
continuous-integration/drone/pr Build is passing Details
2021-06-14 14:41:05 -07:00
Dan Ballard 0a9583e54e linux: be able to use different install configurations
continuous-integration/drone/pr Build is passing Details
2021-06-14 08:45:00 -07:00
Sarah Jamie Lewis c606002541 Merge pull request 'android service and notifications' (#167) from androidservice into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #167
2021-06-11 15:25:53 -07:00
erinn 2abdcdeae0 updatemessageflags got mismerged oops
continuous-integration/drone/pr Build is passing Details
2021-06-11 15:15:42 -07:00
erinn 5c8d448a37 update lcg version
continuous-integration/drone/pr Build is passing Details
2021-06-11 15:05:38 -07:00
erinn 2a97616382 merge trunk
continuous-integration/drone/pr Build is passing Details
2021-06-11 15:00:13 -07:00
erinn 97f1e43009 remove debugging cruft 2021-06-11 14:51:35 -07:00
Sarah Jamie Lewis 9dd9fa98fc Merge pull request 'Persist rejection action' (#166) from rejection_persist into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #166
2021-06-11 14:42:34 -07:00
erinn 37996b5d96 merge
continuous-integration/drone/pr Build is passing Details
2021-06-11 14:29:18 -07:00
erinn 9c69275fe6 android service and notification support take one. ACTION 2021-06-11 14:28:20 -07:00
Sarah Jamie Lewis 421d34e1a7 Merge branch 'trunk' into rejection_persist
continuous-integration/drone/pr Build is passing Details
2021-06-10 14:20:55 -07:00
Sarah Jamie Lewis 0d56c5a3bd Erinns Comments
continuous-integration/drone/pr Build is passing Details
2021-06-10 14:19:48 -07:00
Sarah Jamie Lewis b3dd5e2fee Persist rejection action
continuous-integration/drone/pr Build is passing Details
2021-06-10 11:48:13 -07:00
Dan Ballard 2df34a0192 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into trunk
continuous-integration/drone/push Build is passing Details
2021-06-08 22:27:53 -07:00
Dan Ballard 0e8efc0b6b drone linux dont dup icudtl.dat; windows add recuire VC Redist dlls 2021-06-08 22:27:35 -07:00
Dan Ballard 27f8dd289e Merge pull request 'Fix invitation wrapping' (#157) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #157
2021-06-07 15:34:52 -07:00
Sarah Jamie Lewis aa0a1d9a26 Flutter Upgrade + Translations
continuous-integration/drone/pr Build is passing Details
2021-06-07 15:12:50 -07:00
Sarah Jamie Lewis a4c7fe6ebf Fix Self-Invitations 2021-06-07 15:12:50 -07:00
Sarah Jamie Lewis b0f4a085b8 Fix invitation wrapping 2021-06-07 15:12:50 -07:00
Dan Ballard 332fad4108 android versioncode bump
continuous-integration/drone/push Build is passing Details
2021-06-04 22:38:58 -07:00
Dan Ballard 403eb04124 linux hack my_application.cc so that it does directory exist checks on assets folder and can override to alt paths on the system 2021-06-04 22:38:34 -07:00
Dan Ballard 22eb994c52 Merge pull request 'Update Icons' (#155) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #155
2021-06-04 16:31:21 -07:00
Sarah Jamie Lewis 2c4e65e348 Update Icons
continuous-integration/drone/pr Build is passing Details
2021-06-04 16:09:03 -07:00
Dan Ballard f04404386c Merge pull request 'Clean up message warning logic' (#154) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #154
2021-06-04 15:22:03 -07:00
Sarah Jamie Lewis 1bf7438a26 Merge branch 'trunk' into ui_fixes
continuous-integration/drone/pr Build is passing Details
2021-06-04 15:02:43 -07:00
Sarah Jamie Lewis 3f4c313d73 Fix heart color
continuous-integration/drone/pr Build is passing Details
2021-06-04 15:00:09 -07:00
Sarah Jamie Lewis d27d9b6741 Remove print
continuous-integration/drone/pr Build is passing Details
2021-06-04 14:39:03 -07:00
Sarah Jamie Lewis b2cee259cb Clean conversation warning logic 2021-06-04 14:38:29 -07:00
Dan Ballard edb6ca4e94 Merge pull request 'Disconnected and Ephemeral Warnings' (#153) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #153
2021-06-04 13:25:59 -07:00
Sarah Jamie Lewis dcefa30580 Disconnected and Ephemeral Warnings
continuous-integration/drone/pr Build is passing Details
2021-06-04 13:18:30 -07:00
Dan Ballard a41e6f12b0 Merge pull request 'New experiment checking method.' (#152) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #152
2021-06-04 12:18:57 -07:00
Sarah Jamie Lewis f4f885fcea New experiment checking method.
continuous-integration/drone/pr Build is passing Details
2021-06-04 11:16:09 -07:00
Sarah Jamie Lewis 7351bd322a Merge pull request 'case insensitive search + onion search only from start' (#151) from searchFix into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #151
2021-06-03 21:22:04 -07:00
Dan Ballard 0858e4fd2b case insensitive search + onion search only from start
continuous-integration/drone/pr Build is passing Details
2021-06-03 21:16:54 -07:00
Sarah Jamie Lewis d45dedc76b Merge pull request 'windows: .ico and app name and menu name' (#148) from winnameicon into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #148
2021-06-03 15:44:19 -07:00
Sarah Jamie Lewis 70a049e6fe Merge branch 'trunk' into winnameicon
continuous-integration/drone/pr Build is passing Details
2021-06-03 15:44:11 -07:00
Dan Ballard dbdb62b8e2 windows icon sizing and WNDCLASSEX for small icon
continuous-integration/drone/pr Build is passing Details
2021-06-03 14:34:21 -07:00
Dan Ballard c6c9899a78 windows: .ico and app name and menu name
continuous-integration/drone/pr Build is passing Details
2021-06-03 12:37:17 -07:00
Dan Ballard c37bca5f34 Merge pull request 'Custom Icons + Small Fixes' (#147) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #147
2021-06-03 12:21:51 -07:00
Sarah Jamie Lewis 1eaec8b00a Custom Icons + Small Fixes
continuous-integration/drone/pr Build is passing Details
2021-06-03 12:06:18 -07:00
erinn 89507a8aa6 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into androidservice 2021-06-02 13:21:27 -07:00
erinn 6972028ecf WIP: partial android service migration 2021-06-02 13:21:23 -07:00
erinn 8a69ae9c1c Merge pull request 'Bump version' (#142) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #142
2021-06-02 13:10:52 -07:00
Sarah Jamie Lewis 01819067f3 Bump version
continuous-integration/drone/pr Build is passing Details
2021-06-02 13:08:54 -07:00
erinn 9261119009 Merge pull request 'A few small fixes from user feedback.' (#141) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #141
2021-06-02 13:04:20 -07:00
Sarah Jamie Lewis d7148919ac Merge branch 'trunk' into ui_fixes
continuous-integration/drone/pr Build is passing Details
2021-06-02 12:51:19 -07:00
Sarah Jamie Lewis 0ead306278 upgrade libcwtch
continuous-integration/drone/pr Build is passing Details
2021-06-02 12:50:54 -07:00
Sarah Jamie Lewis ff8dc30f0a Remove unused buttons in message view, add tooltip
continuous-integration/drone/pr Build is passing Details
Fixes: #136
2021-06-02 09:59:44 -07:00
Sarah Jamie Lewis 05c80a82d6 Add message decoration tooltips
continuous-integration/drone/pr Build is passing Details
Fixes: #116
2021-06-02 09:55:00 -07:00
Sarah Jamie Lewis e98ca88359 Fix #107
continuous-integration/drone/pr Build is passing Details
2021-06-02 01:52:54 -07:00
Sarah Jamie Lewis ce6cc4c8b7 Fixup Invite Chrome
continuous-integration/drone/pr Build is passing Details
2021-06-02 01:47:41 -07:00
Dan Ballard 3e27be0b45 Android MainActivity.kt remove debug logs, inc vendorcodee
continuous-integration/drone/push Build is passing Details
2021-06-01 20:00:34 -07:00
Dan Ballard 0a2b6b1773 Android android.bundle.enableUncompressedNativeLibs=false
continuous-integration/drone/push Build is passing Details
2021-06-01 19:38:10 -07:00
Dan Ballard 405280f075 Android android.bundle.enableUncompressedNativeLibs=true
continuous-integration/drone/push Build is passing Details
2021-06-01 19:23:54 -07:00
Dan Ballard ceaa72205c MainActivity test crawl directory trees of potential lib locations for debugging
continuous-integration/drone/push Build is passing Details
2021-06-01 19:03:17 -07:00
Sarah Jamie Lewis cd3af06d3d A few small fixes from user feedback.
continuous-integration/drone/pr Build is passing Details
Fixes: #115
Fixes: #119
2021-06-01 18:21:32 -07:00
Dan Ballard 1e3756daec MainActivity test crawl directory trees of potential lib locations for debugging
continuous-integration/drone/push Build is passing Details
2021-06-01 17:54:12 -07:00
Dan Ballard 6428882ce2 mv libtor.so from libs to jniLibs as per https://www.dynamsoft.com/codepool/build-so-aar-android-studio.html
continuous-integration/drone/push Build is passing Details
2021-06-01 17:03:39 -07:00
Dan Ballard faeb0b8cac remote android lib/arm64 as it's invalid and error causing
continuous-integration/drone/push Build is passing Details
2021-06-01 16:57:51 -07:00
Dan Ballard 9aee5237ca pubspec versioncode bump
continuous-integration/drone/push Build is failing Details
2021-06-01 16:44:45 -07:00
Sarah Jamie Lewis ae8e92c87d Merge pull request 'android: aar needs an libs/arm64 ?' (#133) from aar64 into trunk
continuous-integration/drone/push Build is failing Details
Reviewed-on: #133
2021-06-01 16:43:15 -07:00
Sarah Jamie Lewis c19efbb3d2 Merge branch 'trunk' into aar64
continuous-integration/drone/pr Build is passing Details
2021-06-01 16:43:07 -07:00
Sarah Jamie Lewis 0500773b6c Merge pull request 'version bump for android/play and ffi/linux use locally bundled tor' (#123) from verBumpTor into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #123
2021-06-01 14:57:57 -07:00
Sarah Jamie Lewis 7f33dc8502 Merge branch 'trunk' into verBumpTor
continuous-integration/drone/pr Build is passing Details
2021-06-01 14:57:15 -07:00
Dan Ballard 6c60a93958 version bump for android/play and ffi/linux use locally bundled tor
continuous-integration/drone/pr Build is passing Details
2021-06-01 14:53:13 -07:00
erinn ee303f9d11 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into androidservice 2021-06-01 14:01:56 -07:00
erinn 599993d2d2 WIP: partial android service migration 2021-06-01 14:01:47 -07:00
erinn 3b35b35bce Merge pull request 'New Strings, Updated Message Error UI + Make Acks Larger' (#112) from ui_fixes into trunk
continuous-integration/drone/push Build is passing Details
Reviewed-on: #112
2021-06-01 14:00:01 -07:00
Sarah Jamie Lewis 6a01e94846 New Strings, Updated Message Error UI + Make Acks Larger
continuous-integration/drone/pr Build is passing Details
2021-06-01 13:53:38 -07:00
erinn dfe5f500c6 Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into androidservice 2021-05-28 15:56:54 -07:00
erinn a99a00de30 WIP: partial android service migration 2021-05-28 15:56:45 -07:00
101 changed files with 3496 additions and 2101 deletions

View File

@ -1,174 +1,3 @@
---
kind: pipeline
type: docker
name: linux-android-test
clone:
disable: true
steps:
- name: clone
image: cirrusci/flutter:dev
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 clone gogs@git.openprivacy.ca:flutter/flutter_app.git .
- git checkout $DRONE_COMMIT
- name: fetch
image: cirrusci/flutter:dev
volumes:
- name: deps
path: /root/.pub-cache
commands:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor
- echo `git describe --tags` > VERSION
- echo `date +%G-%m-%d-%H-%M` > BUILDDATE
- flutter pub get
- mkdir deploy
- ./fetch-libcwtch-go.sh
#- name: quality
# image: golang
# volumes:
# - name: deps
# path: /go
# commands:
# - go list ./... | xargs go vet
# - go list ./... | xargs golint
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
- name: build-linux
image: openpriv/flutter-desktop:linux-dev
volumes:
- name: deps
path: /root/.pub-cache
commands:
- flutter build linux --dart-define BUILD_VER=`cat VERSION` --dart-define BUILD_DATE=`cat BUILDDATE`
- mkdir deploy/linux
- cp -r build/linux/x64/release/bundle/* deploy/linux
- cp linux/cwtch.desktop deploy/linux
- cp linux/cwtch.png deploy/linux
- cp linux/libCwtch.so deploy/linux/lib/
- cp /sdks/flutter/bin/cache/artifacts/engine/linux-x64/icudtl.dat deploy/linux
- cp tor deploy/linux
- cd deploy
- mv linux cwtch
- tar -czf cwtch-`cat ../VERSION`.tar.gz cwtch
- rm -r cwtch
- name: build-android
image: cirrusci/flutter:dev
when:
event: push
environment:
upload_jks_file_b64:
from_secret: upload_jks_file_b64
upload_jks_pass:
from_secret: upload_jks_pass
volumes:
- name: deps
path: /root/.pub-cache
commands:
- echo $upload_jks_file_b64 > upload-keystore.jks.b64
- base64 -i --decode upload-keystore.jks.b64 > android/app/upload-keystore.jks
- sed -i "s/%jks-password%/$upload_jks_pass/g" android/key.properties
- flutter build appbundle --dart-define BUILD_VER=`cat VERSION` --dart-define BUILD_DATE=`cat BUILDDATE`
# cant do debug for final release, this is just a stop gap
- flutter build apk --dart-define BUILD_VER=`cat VERSION` --dart-define BUILD_DATE=`cat BUILDDATE`
# or build apk --split-per-abi ?
- cp build/app/outputs/bundle/release/app-release.aab deploy/
- cp build/app/outputs/apk/release/app-release.apk deploy/
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
- name: widget-tests
image: cirrusci/flutter:dev
volumes:
- name: deps
path: /root/.pub-cache
commands:
# - flutter config --enable-linux-desktop
- flutter test --coverage
- genhtml coverage/lcov.info -o coverage/html
# Todo: gonna need more work on container
# https://flutter.dev/desktop
# requirements: Visual Studio 2019 (not to be confused with Visual Studio Code) with the “Desktop development with C++” workload installed, including all of its default components
#- name: build-windows
# image: cirrusci/flutter:dev
#- volumes:
# - name: deps
# path: /root/.pub-cache
# commands:
# - flutter config --enable-windows-desktop
# - flutter build windows
- name: deploy-buildfiles
image: kroniak/ssh-client
environment:
BUILDFILES_KEY:
from_secret: buildfiles_key
secrets: [gogs_account_token]
when:
event: push
status: [ success ]
commands:
- echo $BUILDFILES_KEY > ~/id_rsab64
- base64 -d ~/id_rsab64 > ~/id_rsa
- chmod 400 ~/id_rsa
- export DIR=flwtch-`cat VERSION`-`cat BUILDDATE`
- mv deploy $DIR
- cp -r coverage/html $DIR/coverage-tests
- cp -r test/failures $DIR/test-failures || true
- cd $DIR
- 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/
- name: notify-email
image: drillster/drone-email
settings:
host: build.openprivacy.ca
port: 25
skip_verify: true
from: drone@openprivacy.ca
when:
status: [ failure ]
- name: notify-gogs
image: openpriv/drone-gogs
when:
event: pull_request
status: [ success, changed, failure ]
environment:
GOGS_ACCOUNT_TOKEN:
from_secret: gogs_account_token
settings:
gogs_url: https://git.openprivacy.ca
volumes:
- name: deps
temp: {}
trigger:
repo: flutter/flutter_app
branch: trunk
event:
- push
- pull_request
---
kind: pipeline
type: docker
name: windows
@ -203,45 +32,68 @@ steps:
- name: fetch
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
commands:
- powershell -command "Invoke-WebRequest -Uri https://www.torproject.org/dist/torbrowser/10.0.16/tor-win32-0.4.5.7.zip -OutFile tor.zip"
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '2b7d683f036d0fec149f1d2bdfcf5b7ef4c337005a2b685c056b00047fdb2b57d4c25b8559ad7ef5c7a030b273934be82a9f83ef6e391f5d7d13d8d6c83e8048' ) { Write-Error 'tor.zip sha512sum mismatch' }"
- powershell -command "Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.0.18/tor-win64-0.4.5.9.zip -OutFile tor.zip"
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '72764eb07ad8ab511603aba0734951ca003989f5f4686af91ba220217b4a8a4bcc5f571b59f52c847932f6efedf847b111621983050fcddbb8099d43ca66fb07' ) { Write-Error 'tor.zip sha512sum mismatch' }"
- git describe --tags > VERSION
- powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE
- .\fetch-libcwtch-go.ps1
-
- name: build-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
commands:
- flutter pub get
# flwtch-`cat VERSION`-`cat BUILDDATE`
- $Env:buildname = 'flwtch-win-'
- $Env:version += type .\VERSION
- $Env:buildname += $Env:version
- $Env:buildname += '-'
- $Env:builddate += type .\BUILDDATE
- $Env:buildname += $Env:builddate
- $Env:builddir += $Env:buildname
- $Env:zip = 'cwtch-'
- $Env:zip += $Env:version
- $Env:zip += '.zip'
- $Env:sha = $Env:zip
- $Env:sha += '.sha512'
- $Env:releasedir = "build\\windows\\runner\\Release\\"
- flutter build windows --dart-define BUILD_VER=$Env:version --dart-define BUILD_DATE=$Env:builddate
- copy windows\libCwtch.dll $Env:releasedir
# 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
- powershell -command "Expand-Archive -Path tor.zip -DestinationPath $Env:releasedir\Tor"
- name: package-windows
image: openpriv/nsis:latest
environment:
pfx:
from_secret: pfx
pfx_pass:
from_secret: pfx_pass
commands:
- $Env:version += type .\VERSION
- $Env:builddate += type .\BUILDDATE
- $Env:releasedir = "build\\windows\\runner\\Release\\"
- $Env:zip = 'cwtch-' + $Env:version + '.zip'
- $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:builddir = $Env:buildname
- echo $Env:pfx > codesign.pfx.b64
- certutil -decode codesign.pfx.b64 codesign.pfx
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\cwtch.exe
- copy windows\runner\resources\knot_128.ico $Env:releasedir\cwtch.ico
- makensis windows\nsis\cwtch-installer.nsi
- move windows\nsis\cwtch-installer.exe cwtch-installer.exe
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com cwtch-installer.exe
- powershell -command "(Get-FileHash cwtch-installer.exe -Algorithm sha512).Hash" > cwtch-installer.sha512
- mkdir deploy
- move build\\windows\\runner\\Release $Env:builddir
- copy windows\libCwtch.dll $Env:builddir
- powershell -command "Expand-Archive -Path tor.zip -DestinationPath $Env:builddir\Tor"
- powershell -command "Compress-Archive -Path $Env:builddir -DestinationPath $Env:zip"
- powershell -command "(Get-FileHash *.zip -Algorithm sha512).Hash" > $Env:sha
- mkdir deploy\$Env:builddir
- move $Env:zip deploy\$Env:builddir
- move $Env:sha deploy\$Env:builddir
- move $Env:releasedir $Env:builddir
- powershell -command "Compress-Archive -Path $Env:builddir -DestinationPath cwtch.zip"
- powershell -command "(Get-FileHash cwtch.zip -Algorithm sha512).Hash" > $Env:zipsha
- move cwtch-installer.exe deploy\$Env:builddir\cwtch-installer.exe
- move cwtch.zip deploy\$Env:builddir\$Env:zip
- move *.sha512 deploy\$Env:builddir
- name: deploy-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
when:
event: push
status: [ success ]
event: push
status: [ success ]
environment:
BUILDFILES_KEY:
from_secret: buildfiles_key
@ -254,4 +106,4 @@ trigger:
repo: flutter/flutter_app
branch: trunk
event:
- push
- push

View File

@ -1 +1 @@
v0.0.2-45-g4f625c7-2021-05-31-23-30
v0.0.2-108-g3964348-2021-06-24-17-42

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2021 Open Privacy Research Society
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,6 +1,6 @@
# flwtch
# Cwtch UI
A new Flutter application.
A Flutter based Cwtch UI
## Getting Started
@ -8,13 +8,19 @@ click the play button in android studio
### Linux
- libCwtch-go: the result of `make linux`, `libCwtch.so` should be in the link path
- libCwtch-go: required to be on the link path (linux/cwtch.destktop demonstrates with `env LD_LIBRARY_PATH=./lib/` on the front of the comman)
- fetch-libcwtch-go.sh will fetch a prebuilt version
- or compile from source from libcwtch-go with `make linux`
- `tor` should be in the PATH
### Windows
- libCwtch-go: the result of `make windows`, `libCwtch.dll` should be placed in the source root
- tor is bundled in `windors/Tor`
- run `fetch-libcwtch-go.ps1` to get `libCwtch.dll` which is required to run
- run `fetch-tor-win.ps1` to fetch Tor for windows
#### Issues
- Flutter engine has a [known bug](https://github.com/flutter/flutter/issues/75675) around the Right Shift key being sticky. We have implemented the mostly work around, but until it is fixed, right shift occasionally acts permenent. If this happens, just tap left shift and it will reset
## l10n
@ -26,14 +32,24 @@ After adding a new key and providing/obtaining translations for it, follow the n
### Updating translations
Only Open Privacy staff members can update translations automatically:
Only Open Privacy staff members can update translations.
```
flutter pub run flutter_lokalise download -v --api-token "<X>" --project-id "<Y>"
```
In Lokalise, hit Download and make sure:
This will download a bundle of translations from Lokalise and convert it to resource files in `lib/l10n/intl_*.arb`.
The next time Flwtch is built, Flutter will notice the changes and update `app_localizations.dart` accordingly (thanks to `generate:true` in `pubspec.yaml`).
* Format is set to "Flutter (.arb)
* Output filename is set to `l10n/intl_%LANG_ISO%.%FORMAT%`
* Empty translations is set to "Replace with base language"
Build, download and unzip the output, overwriting `lib/l10n`. The next time Flwtch is built, Flutter will notice the changes and update `app_localizations.dart` accordingly (thanks to `generate:true` in `pubspec.yaml`).
### Adding a language
If a new language has been added to the Lokalise project, two additional manual steps need to be done:
* Create a new key called `localeXX` for the name of the language
* Add it to the settings pane by updating `getLanguageFull()` in `lib/views/globalsettingsview.dart`
Then rebuild as normal.
### Using a string
@ -51,6 +67,5 @@ Text(AppLocalizations.of(context)!.stringIdentifer),
### Configuration
API tokens are only available to Open Privacy staff at this time, who will perform the translation updates for you as part of merging your PRs.
With `generate: true` in `pubspec.yaml`, the Flutter build process checks `l10n.yaml` for input/output filenames.

50
SPEC.md
View File

@ -8,7 +8,7 @@ required - any new Cwtch work is beyond the scope of this initial spec.
# Functional Requirements
- [ ] Kill all processes / isolates on exit (Blocked - P1)
- [ ] Android Service? (P1)
- [X] Android Service? (P1)
# Splash Screen
- [X] Android
@ -16,9 +16,9 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [ ] Desktop (P2)
# Custom Styled Widgets
- [/] Label Widget
- [X] Label Widget
- [X] Initial
- [ ] With Accessibility / Zoom Integration (P1)
- [X] With Accessibility / Zoom Integration (P1)
- [X] Text Field Widget
- [X] Password Widget
- [X] Text Button Widget (for Copy)
@ -33,10 +33,10 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Profile Picture
- [X] default images
- [ ] custom images (P3)
- [ ] coloured ring border (P2)
- [X] coloured ring border (P2)
- [X] Profile Name
- [X] Edit Button
- [ ] Unread messages badge (P2)
- [X Unread messages badge (P2)
- [X] Navigate to a specific Profile Contacts Pane (when clicking on a Profile row)
- [X] Navigate to a specific Profile Management Pane (edit Button)
- [X] Navigate to the Settings Pane (Settings Button in Action bar)
@ -54,12 +54,12 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Update Profile Name
- [X] Update Profile Password
- [ ] Error Message When Attempting to Update Password with Wrong Old Password (P2)
- [X] Error Message When Attempting to Update Password with Wrong Old Password (P2)
- [ ] Easy Transition from Unencrypted Profile -> Encrypted Profile (P3)
- [ ] Delete a Profile (P2)
- [ ] Dialog Acknowledgement (P2)
- [ ] Require Old Password Gate (P2)
- [ ] Async Checking of Password (P2)
- [X] Delete a Profile (P2)
- [X] Dialog Acknowledgement (P2)
- [X] Require Old Password Gate (P2)
- [X] Async Checking of Password (P2)
- [X] Copy Profile Onion Address
## Profile Pane (formally Contacts Pane)
@ -76,11 +76,11 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Name
- [X] Onion
- [X] Online Status
- [ ] Unread Messages Badge (P1)
- [ ] In Order of Most Recent Message / Activity (P1)
- [ ] With Accept / Reject Heart/Trash Bin Option (P1)
- [ ] Separate list area for Blocked Contacts (P1)
- [ ] Display all Group Contacts (if experiment is enabled)
- [X] Unread Messages Badge (P1)
- [X] In Order of Most Recent Message / Activity (P1)
- [X] With Accept / Reject Heart/Trash Bin Option (P1)
- [X] Separate list area for Blocked Contacts (P1)
- [X] Display all Group Contacts (if experiment is enabled)
- [X] Navigate to a specific Contact or Group Message Pane (Contact Row)
- [X] Pressing Back should go back to the home pane
@ -88,22 +88,22 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Allowing Copying the Profile Onion Address for Sharing
- [X] Allowing Pasting a Peer Onion Address for adding to Contacts
- [ ] (with optional name field)
- [ ] Allowing Pasting a Group Invite / Server Address
- [X] Allowing Pasting a Group Invite / Server Address
- [X] (if group experiment is enabled)
## Message Overlay
- [X] Display Messages from Contacts
- [ ] Allowing copying the text of a specific message (on mobile) (P2)
- [X] Allowing copying the text of a specific message (on mobile) (P2)
- [X] Send a message to the specific Contact / Group
- [~] Display the Acknowledgement status of a message (P1)
- [ ] Navigate to the specific Contact or Group Settings Pane ( Settings Button in Action bar)
- [X] Navigate to the specific Contact or Group Settings Pane ( Settings Button in Action bar)
- [ ] Emoji Support (P1)
- [ ] Display in-message emoji text labels e.g. `:label:` as emoji. (P1)
- [ ] Functional Emoji Drawer Widget for Selection (P2)
- [ ] Mutant Standard? (P2)
- [ ] Display a warning if Contact / Server is offline (Broken Heart) (P1)
- [ ] Display a warning for configuring peer history (P2)
- [X] Display a warning if Contact / Server is offline (Broken Heart) (P1)
- [X] Display a warning for configuring peer history (P2)
- [X] Pressing Back should go back to the contacts pane
## List Overlay (P3)
@ -123,11 +123,11 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Pressing Back should go back to the message pane
## Group Settings Pane (experimental - P3)
- [ ] Gated behind group experiment
- [ ] Update local name of group
- [ ] Get Group Invite
- [ ] Leave Group
- [ ] Pressing Back should go back to the message pane for the group
- [X] Gated behind group experiment
- [X] Update local name of group
- [X] Get Group Invite
- [X] Leave Group
- [X] Pressing Back should go back to the message pane for the group

View File

@ -69,6 +69,15 @@ android {
signingConfig signingConfigs.release
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
flutter {
@ -82,4 +91,30 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
implementation "com.airbnb.android:lottie:3.5.0"
implementation "com.android.support.constraint:constraint-layout:2.0.4"
// WorkManager
// (Java only)
//implementation("androidx.work:work-runtime:$work_version")
// Kotlin + coroutines
implementation("androidx.work:work-runtime-ktx:2.5.0")
// optional - RxJava2 support
//implementation("androidx.work:work-rxjava2:$work_version")
// optional - GCMNetworkManager support
//implementation("androidx.work:work-gcm:$work_version")
// optional - Test helpers
//androidTestImplementation("androidx.work:work-testing:$work_version")
// optional - Multiprocess support
implementation "androidx.work:work-multiprocess:2.5.0"
// end of workmanager deps
// needed to prevent a ListenableFuture dependency conflict/bug
// see https://github.com/google/ExoPlayer/issues/7905#issuecomment-692637059
implementation 'com.google.guava:guava:any'
}

View File

@ -43,4 +43,9 @@
<!--Needed to access Tor socket-->
<uses-permission android:name="android.permission.INTERNET" />
<!--Needed to run in background (lol)-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" />
</manifest>

View File

@ -0,0 +1,266 @@
package im.cwtch.flwtch
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.*
import cwtch.Cwtch
import io.flutter.FlutterInjector
import org.json.JSONObject
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
private var notificationID: MutableMap<String, Int> = mutableMapOf()
private var notificationIDnext: Int = 1
override suspend fun doWork(): Result {
val method = inputData.getString(KEY_METHOD)
?: return Result.failure()
val args = inputData.getString(KEY_ARGS)
?: return Result.failure()
// Mark the Worker as important
val progress = "Cwtch is keeping Tor running in the background"//todo:translate
setForeground(createForegroundInfo(progress))
return handleCwtch(method, args)
}
private fun getNotificationID(profile: String, contact: String): Int {
val k = "$profile $contact"
if (!notificationID.containsKey(k)) {
notificationID[k] = notificationIDnext++
}
return notificationID[k] ?: -1
}
private fun handleCwtch(method: String, args: String): Result {
val a = JSONObject(args)
when (method) {
"Start" -> {
Log.i("FlwtchWorker.kt", "handleAppInfo Start")
val appDir = (a.get("appDir") as? String) ?: ""
val torPath = (a.get("torPath") as? String) ?: "tor"
Log.i("FlwtchWorker.kt", "appDir: '$appDir' torPath: '$torPath'")
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
while(true) {
Log.i("FlwtchWorker.kt", "while(true)getAppbusEvent()")
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
val data = JSONObject(evt.Data)
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createMessageNotificationChannel(data.getString("RemotePeer"), data.getString("RemotePeer"))
} 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 loader = FlutterInjector.instance().flutterLoader()
val key = loader.getLookupKeyForAsset("assets/"+data.getString("Picture"))//"assets/profiles/001-centaur.png")
val fh = applicationContext.assets.open(key)
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "NotificationClicked")
intent.putExtra("ProfileOnion", data.getString("ProfileOnion"))
intent.putExtra("RemotePeer", if (evt.EventType == "NewMessageFromPeer") data.getString("RemotePeer") else data.getString("GroupID"))
}
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(data.getString("Nick"))
.setContentText("New message")//todo: translate
.setLargeIcon(BitmapFactory.decodeStream(fh))
.setSmallIcon(R.mipmap.knott)
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.build()
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), data.getString("RemotePeer")), newNotification)
}
Intent().also { intent ->
intent.action = "im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS"
intent.putExtra("EventType", evt.EventType)
intent.putExtra("Data", evt.Data)
intent.putExtra("EventID", evt.EventID)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
}
}
}
"ReconnectCwtchForeground" -> {
Cwtch.reconnectCwtchForeground()
}
"CreateProfile" -> {
val nick = (a.get("nick") as? String) ?: ""
val pass = (a.get("pass") as? String) ?: ""
Cwtch.createProfile(nick, pass)
}
"LoadProfiles" -> {
val pass = (a.get("pass") as? String) ?: ""
Cwtch.loadProfiles(pass)
}
"GetMessage" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val indexI = a.getInt("index")
Log.i("FlwtchWorker.kt", "indexI = $indexI")
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build())
}
"UpdateMessageFlags" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val midx = (a.get("midx") as? Long) ?: 0
val flags = (a.get("flags") as? Long) ?: 0
Cwtch.updateMessageFlags(profile, handle, midx, flags)
}
"AcceptContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
Cwtch.acceptContact(profile, handle)
}
"BlockContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
Cwtch.blockContact(profile, handle)
}
"SendMessage" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val message = (a.get("message") as? String) ?: ""
Cwtch.sendMessage(profile, handle, message)
}
"SendInvitation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val target = (a.get("target") as? String) ?: ""
Cwtch.sendInvitation(profile, handle, target)
}
"SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: ""
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
Cwtch.sendProfileEvent(onion, jsonEvent)
}
"SendAppEvent" -> {
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
Cwtch.sendAppEvent(jsonEvent)
}
"ResetTor" -> {
Cwtch.resetTor()
}
"ImportBundle" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val bundle = (a.get("bundle") as? String) ?: ""
Cwtch.importBundle(profile, bundle)
}
"SetGroupAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
val key = (a.get("key") as? String) ?: ""
val value = (a.get("value") as? String) ?: ""
Cwtch.setGroupAttribute(profile, groupHandle, key, value)
}
"CreateGroup" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val server = (a.get("server") as? String) ?: ""
val groupName = (a.get("groupname") as? String) ?: ""
Cwtch.createGroup(profile, server, groupName)
}
"DeleteProfile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val pass = (a.get("pass") as? String) ?: ""
Cwtch.deleteProfile(profile, pass)
}
"LeaveConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val contactHandle = (a.get("contactHandle") as? String) ?: ""
Cwtch.leaveConversation(profile, contactHandle)
}
"LeaveGroup" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.leaveGroup(profile, groupHandle)
}
"RejectInvite" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.rejectInvite(profile, groupHandle)
}
"Shutdown" -> {
Cwtch.shutdownCwtch();
return Result.success()
}
else -> return Result.failure()
}
return Result.success()
}
// Creates an instance of ForegroundInfo which can be used to update the
// ongoing notification.
private fun createForegroundInfo(progress: String): ForegroundInfo {
val id = "flwtch"
val title = "Flwtch"
val cancel = "Shut down"//todo: translate
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createForegroundNotificationChannel(id, id)
} 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)
""
}
// This PendingIntent can be used to cancel the worker
val intent = WorkManager.getInstance(applicationContext)
.createCancelPendingIntent(getId())
val notification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(title)
.setTicker(title)
.setContentText(progress)
.setSmallIcon(R.mipmap.knott)
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build()
return ForegroundInfo(101, notification)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createForegroundNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE)
chan.lightColor = Color.MAGENTA
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
notificationManager.createNotificationChannel(chan)
return channelId
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createMessageNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
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

@ -1,32 +1,29 @@
package im.cwtch.flwtch
import SplashView
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.annotation.NonNull
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Looper
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import cwtch.Cwtch
import io.flutter.plugin.common.EventChannel
import kotlin.concurrent.thread
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView()
// Channel to get app info
@ -39,13 +36,37 @@ class MainActivity: FlutterActivity() {
// Channel to send eventbus events on
private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus"
// Channel to trigger contactview when an external notification is clicked
private val CHANNEL_NOTIF_CLICK = "im.cwtch.flwtch/notificationClickHandler"
// WorkManager tag applied to all Start() infinite coroutines
val WORKER_TAG = "cwtchEventBusWorker"
private var myReceiver: MyBroadcastReceiver? = null
private var methodChan: MethodChannel? = null
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (methodChan == null || intent.extras == null) return
if (!intent.extras!!.containsKey("ProfileOnion") || !intent.extras!!.containsKey("RemotePeer")) {
Log.i("onNewIntent", "got intent with no onions")
return
}
val profile = intent.extras!!.getString("ProfileOnion")
val handle = intent.extras!!.getString("RemotePeer")
val mappo = mapOf("ProfileOnion" to profile, "RemotePeer" to handle)
val j = JSONObject(mappo)
methodChan!!.invokeMethod("NotificationClicked", j.toString())
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Note: this methods are invoked on the main thread.
//note to self: ask someone if this does anything ^ea
requestWindowFeature(Window.FEATURE_NO_TITLE)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) }
methodChan = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NOTIF_CLICK)
}
private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) {
@ -63,139 +84,88 @@ class MainActivity: FlutterActivity() {
return ainfo.nativeLibraryDir
}
// receives messages from the ForegroundService (which provides, ironically enough, the backend)
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"Start" -> {
Log.i("MainActivity.kt", "handleAppInfo Start")
val appDir = (call.argument("appDir") as? String) ?: "";
val torPath = (call.argument("torPath") as? String) ?: "tor";
Log.i("MainActivity.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'")
Cwtch.startCwtch(appDir, torPath)
var method = call.method
val argmap: Map<String, String> = call.arguments as Map<String, String>
// seperate coroutine to poll event bus and send to dart
val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS)
Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...")
GlobalScope.launch(Dispatchers.IO) {
while(true) {
val evt = AppbusEvent(Cwtch.getAppBusEvent())
Log.i("MainActivity.kt", "got appbusEvent: " + evt)
launch(Dispatchers.Main) {
//todo: this elides evt.EventID which may be needed at some point?
eventbus_chan.invokeMethod(evt.EventType, evt.Data)
}
}
// the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start()
// in case the ForegroundService is still running. in both cases, however, we *do* want to re-register
// the eventbus listener.
if (call.method == "Start") {
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get
// accidentally duplicated. however, we still need to manually check if it's running or not, so
// that we can divert this method call to ReconnectCwtchForeground instead if so.
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
for (workInfo in works) {
Log.i("handleCwtch:WorkManager", "$workInfo")
if (!workInfo.tags.contains(uniqueTag)) {
Log.i("handleCwtch:WorkManager", "canceling ${workInfo.id} bc tags don't include $uniqueTag")
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
}
}
"SelectProfile" -> {
val onion = (call.argument("profile") as? String) ?: "";
Cwtch.selectProfile(onion)
}
"CreateProfile" -> {
val nick = (call.argument("nick") as? String) ?: "";
val pass = (call.argument("pass") as? String) ?: "";
Cwtch.createProfile(nick, pass)
}
"LoadProfiles" -> {
val pass = (call.argument("pass") as? String) ?: "";
Cwtch.loadProfiles(pass)
}
"GetProfiles" -> result.success(Cwtch.getProfiles())
// "ACNEvents" -> result.success(Cwtch.acnEvents())
"ContactEvents" -> result.success(Cwtch.contactEvents())
"NumMessages" -> {
val profile = (call.argument("profile") as? String) ?: "";
val handle = (call.argument("contact") as? String) ?: "";
result.success(Cwtch.numMessages(profile, handle))
}
"GetMessage" -> {
//Log.i("MainActivivity.kt", (call.argument("index")));
WorkManager.getInstance(this).pruneWork()
// var args : HashMap<String, dynamic> = call.arguments();
// Log.i("MainActivity.kt", args);
val profile = (call.argument("profile") as? String) ?: "";
val handle = (call.argument("contact") as? String) ?: "";
val indexI = call.argument<Int>("index") ?: 0;
Log.i("MainActivity.kt", "indexI = " + indexI)
result.success(Cwtch.getMessage(profile, handle, indexI.toLong()))
}
"GetMessages" -> {
val profile = (call.argument("profile") as? String) ?: "";
val handle = (call.argument("contact") as? String) ?: "";
val start = (call.argument("start") as? Long) ?: 0;
val end = (call.argument("end") as? Long) ?: 0;
result.success(Cwtch.getMessages(profile, handle, start, end))
}
"AcceptContact" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
Cwtch.acceptContact(profile, handle);
}
"BlockContact" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
Cwtch.blockContact(profile, handle);
}
"DebugResetContact" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
Cwtch.debugResetContact(profile, handle);
}
"SendMessage" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
val message = (call.argument("message") as? String) ?: "";
Cwtch.sendMessage(profile, handle, message);
}
"SendInvitation" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
val target = (call.argument("target") as? String) ?: "";
Cwtch.sendInvitation(profile, handle, target);
}
"SendProfileEvent" -> {
val onion = (call.argument("onion") as? String) ?: "";
val jsonEvent = (call.argument("jsonEvent") as? String) ?: "";
Cwtch.sendProfileEvent(onion, jsonEvent);
}
"SendAppEvent" -> {
val jsonEvent = (call.argument("jsonEvent") as? String) ?: "";
Cwtch.sendAppEvent(jsonEvent);
}
"ResetTor" -> {
Cwtch.resetTor();
}
"ImportBundle" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val bundle = (call.argument("bundle") as? String) ?: "";
Cwtch.importBundle(profile, bundle);
}
"SetGroupAttribute" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val groupHandle = (call.argument("groupHandle") as? String) ?: "";
val key = (call.argument("key") as? String) ?: "";
val value = (call.argument("value") as? String) ?: "";
Cwtch.setGroupAttribute(profile, groupHandle, key, value);
}
"CreateGroup" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val server = (call.argument("server") as? String) ?: "";
val groupName = (call.argument("groupname") as? String) ?: "";
Cwtch.createGroup(profile, server, groupName);
}
"LeaveGroup" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val groupHandle = (call.argument("groupHandle") as? String) ?: "";
Cwtch.leaveGroup(profile, groupHandle);
}
"RejectInvite" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val groupHandle = (call.argument("groupHandle") as? String) ?: "";
Cwtch.rejectInvite(profile, groupHandle);
}
else -> result.notImplemented()
Log.i("MainActivity.kt", "Start() launching foregroundservice")
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
// 15 minutes is the shortest interval you can request
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
}
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(this).enqueue(workRequest)
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
this, Observer { workInfo ->
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
val res = workInfo.outputData.keyValueMap.toString()
result.success(workInfo.outputData.getString("result"))
}
}
)
}
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver
override fun onResume() {
super.onResume()
Log.i("MainActivity.kt", "onResume")
if (myReceiver == null) {
Log.i("MainActivity.kt", "onResume registering local broadcast receiver / event bus forwarder")
val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS)
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS")
myReceiver = MyBroadcastReceiver(mc)
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(myReceiver!!, filter)
}
// ReconnectCwtchForeground which will resync counters and settings...
// We need to do this here because after a "pause" flutter is still running
// but we might have lost sync with the background process...
Log.i("MainActivity.kt", "Call ReconnectCwtchForeground")
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, "ReconnectCwtchForeground").putString(FlwtchWorker.KEY_ARGS, "{}").build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(applicationContext).enqueue(workRequest)
}
override fun onStop() {
super.onStop()
Log.i("MainActivity.kt", "onStop")
if (myReceiver != null) {
LocalBroadcastManager.getInstance(applicationContext).unregisterReceiver(myReceiver!!);
myReceiver = null;
}
}
override fun onDestroy() {
super.onDestroy()
Log.i("MainActivity.kt", "onDestroy - cancelling all WORKER_TAG and pruning old work")
WorkManager.getInstance(this).cancelAllWorkByTag(WORKER_TAG)
WorkManager.getInstance(this).pruneWork()
}
// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095
@ -217,4 +187,17 @@ class MainActivity: FlutterActivity() {
val EventID = this.optString("EventID")
val Data = this.optString("Data")
}
// MainActivity.MyBroadcastReceiver receives events from the Cwtch service via im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS Android local broadcast intents
// then it forwards them to the flutter ui engine using the CWTCH_EVENTBUS methodchannel
class MyBroadcastReceiver(mc: MethodChannel) : BroadcastReceiver() {
val eventBus: MethodChannel = mc
override fun onReceive(context: Context, intent: Intent) {
val evtType = intent.getStringExtra("EventType") ?: ""
val evtData = intent.getStringExtra("Data") ?: ""
//val evtID = intent.getStringExtra("EventID") ?: ""//todo?
eventBus.invokeMethod(evtType, evtData)
}
}
}

View File

@ -11,7 +11,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:lottie_autoPlay="true"
app:lottie_rawRes="@raw/cwtch_animated_logo"
app:lottie_rawRes="@raw/cwtch_animated_logo_op"
app:lottie_loop="true"
app:lottie_speed="1.00" />

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -27,6 +27,7 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
//removed due to gradle namespace conflicts that are beyond erinn's mere mortal understanding
//task clean(type: Delete) {
// delete rootProject.buildDir
//}

View File

@ -2,3 +2,4 @@ org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true
android.bundle.enableUncompressedNativeLibs=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -17,10 +17,10 @@
style="enable-background:new 0 0 24 24;"
xml:space="preserve"
sodipodi:docname="negative_heart_24px.svg"
inkscape:export-filename="/home/sarah/AndroidStudioProjects/flutter_app/assets/core/negative_heart_512px.png"
inkscape:export-xdpi="4096"
inkscape:export-ydpi="4096"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata
inkscape:export-filename="/home/sarah/PARA/projects/cwtch/assets/core/negative_heart_256px.png"
inkscape:export-xdpi="1024"
inkscape:export-ydpi="1024"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
@ -34,14 +34,14 @@
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-height="1020"
id="namedview10"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="14.687942"
inkscape:cx="-5.8983051"
inkscape:cy="14.281162"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-y="31"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
assets/cwtch_title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
assets/fonts/CwtchIcons.ttf Normal file

Binary file not shown.

View File

@ -1,6 +1,6 @@
Invoke-WebRequest -Uri https://www.torproject.org/dist/torbrowser/10.0.16/tor-win32-0.4.5.7.zip -OutFile tor.zip
Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.0.18/tor-win64-0.4.5.9.zip -OutFile tor.zip
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '2b7d683f036d0fec149f1d2bdfcf5b7ef4c337005a2b685c056b00047fdb2b57d4c25b8559ad7ef5c7a030b273934be82a9f83ef6e391f5d7d13d8d6c83e8048' ) { Write-Error 'tor.zip sha512sum mismatch' }
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '72764eb07ad8ab511603aba0734951ca003989f5f4686af91ba220217b4a8a4bcc5f571b59f52c847932f6efedf847b111621983050fcddbb8099d43ca66fb07' ) { Write-Error 'tor.zip sha512sum mismatch' }
Expand-Archive -Path tor.zip -DestinationPath Tor

12
fetch-tor.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.5.9-linux-x86_64 -O linux/tor
chmod a+x linux/tor
mkdir -p android/app/src/main/jniLibs/arm64-v8a
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.4.9-arm64_pie -O android/app/src/main/jniLibs/arm64-v8a/libtor.so
chmod a+x android/app/src/main/jniLibs/arm64-v8a/libtor.so
mkdir -p android/app/src/main/jniLibs/armeabi-v7a
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.4.9-arm_pie -O android/app/src/main/jniLibs/armeabi-v7a/libtor.so
chmod a+x android/app/src/main/jniLibs/armeabi-v7a/libtor.so

View File

@ -1,6 +1,10 @@
import 'package:flutter/src/services/text_input.dart';
abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<void> Start();
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground();
// ignore: non_constant_identifier_names
void SelectProfile(String onion);
@ -8,6 +12,8 @@ abstract class Cwtch {
void CreateProfile(String nick, String pass);
// ignore: non_constant_identifier_names
void LoadProfiles(String pass);
// ignore: non_constant_identifier_names
void DeleteProfile(String onion, String pass);
// ignore: non_constant_identifier_names
void ResetTor();
@ -22,28 +28,19 @@ abstract class Cwtch {
void AcceptContact(String profileOnion, String contactHandle);
// ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle);
// ignore: non_constant_identifier_names
void DebugResetContact(String profileOnion, String contactHandle);
// ignore: non_constant_identifier_names
Future<dynamic> ACNEvents();
// ignore: non_constant_identifier_names
Future<dynamic> ContactEvents();
// ignore: non_constant_identifier_names
Future<dynamic> GetProfiles();
// ignore: non_constant_identifier_names
Future<dynamic> NumMessages(String profile, String handle);
// ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index);
// ignore: non_constant_identifier_names
Future<dynamic> GetMessages(String profile, String handle, int start, int end);
void UpdateMessageFlags(String profile, String handle, int index, int flags);
// ignore: non_constant_identifier_names
void SendMessage(String profile, String handle, String message);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, String handle, String target);
// ignore: non_constant_identifier_names
void LeaveConversation(String profile, String handle);
// ignore: non_constant_identifier_names
void CreateGroup(String profile, String server, String groupName);
// ignore: non_constant_identifier_names
@ -56,5 +53,8 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle);
// ignore: non_constant_identifier_names
void Shutdown();
void dispose();
}

View File

@ -17,20 +17,28 @@ class CwtchNotifier {
late ErrorHandler error;
late TorStatus torStatus;
late NotificationsManager notificationManager;
late AppState appState;
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP) {
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN) {
profileCN = pcn;
settings = settingsCN;
error = errorCN;
torStatus = torStatusCN;
notificationManager = notificationManagerP;
appState = appStateCN;
}
void handleMessage(String type, dynamic data) {
switch (type) {
case "CwtchStarted":
appState.SetCwtchInit();
break;
case "CwtchStartError":
appState.SetAppError(data["Error"]);
break;
case "NewPeer":
profileCN.add(ProfileInfoState(
onion: data["Identity"], nickname: data["name"], imagePath: data["picture"], contactsJson: data["ContactsJson"], serversJson: data["ServerList"], online: data["Online"] == "true"));
// if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta...
profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword");
break;
case "PeerCreated":
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(
@ -57,13 +65,20 @@ class CwtchNotifier {
if (serverInfoState != null) {
status = serverInfoState.status;
}
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["GroupID"],
isInvitation: false, imagePath: data["PicturePath"], nickname: data["GroupName"], status: status, server: data["GroupServer"], isGroup: true, lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
}
break;
case "PeerDeleted":
profileCN.delete(data["Identity"]);
// todo standarize
error.handleUpdate("deleteprofile.success");
break;
case "DeleteContact":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["RemotePeer"]);
break;
case "DeleteGroup":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["GroupID"]);
break;
@ -83,7 +98,9 @@ class CwtchNotifier {
break;
case "NewMessageFromPeer":
notificationManager.notify("New Message From Peer!");
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++;
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
break;
@ -108,7 +125,9 @@ class CwtchNotifier {
case "NewMessageFromGroup":
if (data["ProfileOnion"] != data["RemotePeer"]) {
//not from me
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
} else {
@ -125,6 +144,11 @@ class CwtchNotifier {
}
}
break;
case "MessageCounterResync":
var contactHandle = data["RemotePeer"];
if (contactHandle == null || contactHandle == "") contactHandle = data["GroupID"];
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = int.parse(data["Data"]);
break;
case "IndexedFailure":
print("IndexedFailure: $data");
var idx = data["Index"];
@ -152,7 +176,10 @@ class CwtchNotifier {
break;
case "AppError":
print("New App Error: $data");
if (data["Data"] != null) {
// special case for delete error (todo: standardize cwtch errors)
if (data["Error"] == "Password did not match") {
error.handleUpdate("deleteprofile.error");
} else if (data["Data"] != null) {
error.handleUpdate(data["Data"]);
}
break;
@ -174,27 +201,32 @@ class CwtchNotifier {
print("acn status: $data");
torStatus.handleUpdate(int.parse(data["Progress"]), data["Status"]);
break;
case "ACNVersion":
print("acn version: $data");
torStatus.updateVersion(data["Data"]);
break;
case "UpdateServerInfo":
profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]);
break;
case "NewGroup":
print("new group invite: $data");
print("new group: $data");
String invite = data["GroupInvite"].toString();
if (invite.startsWith("torv3")) {
String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5)));
dynamic groupInvite = jsonDecode(inviteJson);
print("new group invite: $groupInvite");
print("group invite: $groupInvite");
// Retrieve Server Status from Cache...
String status = "";
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(groupInvite["ServerHost"]);
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]);
if (serverInfoState != null) {
print("Got server status: " + serverInfoState.status);
status = serverInfoState.status;
}
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"],
isInvitation: true,
isInvitation: false,
imagePath: data["PicturePath"],
nickname: groupInvite["GroupName"],
server: groupInvite["ServerHost"],
@ -207,6 +239,7 @@ class CwtchNotifier {
break;
case "AcceptGroupInvite":
print("accept group invite: $data");
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.isInvitation = false;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
break;

View File

@ -4,6 +4,7 @@ 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';
@ -15,8 +16,11 @@ import '../config.dart';
/// Cwtch API ///
/////////////////////
typedef start_cwtch_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StartCwtchFn = void Function(Pointer<Utf8> dir, int len, Pointer<Utf8> tor, int torLen);
typedef start_cwtch_function = Int8 Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StartCwtchFn = int Function(Pointer<Utf8> dir, int len, Pointer<Utf8> tor, int torLen);
typedef void_from_void_funtion = Void Function();
typedef VoidFromVoidFunction = void Function();
typedef void_from_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
@ -27,6 +31,9 @@ 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_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();
@ -54,13 +61,14 @@ typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int,
typedef get_json_blob_from_str_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
typedef acn_events_function = Pointer<Utf8> Function();
typedef ACNEventsFn = Pointer<Utf8> Function();
typedef appbus_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> Function();
class CwtchFfi implements Cwtch {
late DynamicLibrary library;
late CwtchNotifier cwtchNotifier;
late Isolate cwtchIsolate;
ReceivePort _receivePort = ReceivePort();
CwtchFfi(CwtchNotifier _cwtchNotifier) {
if (Platform.isWindows) {
@ -82,6 +90,7 @@ class CwtchFfi implements Cwtch {
Map<String, String> envVars = Platform.environment;
if (Platform.isLinux) {
home = (envVars['HOME'])!;
bundledTor = "./tor";
} else if (Platform.isWindows) {
home = (envVars['UserProfile'])!;
bundledTor = "Tor\\Tor\\tor.exe";
@ -100,7 +109,6 @@ class CwtchFfi implements Cwtch {
StartCwtch(ut8CwtchDir, ut8CwtchDir.length, bundledTor.toNativeUtf8(), bundledTor.length);
// Spawn an isolate to listen to events from libcwtch-go and then dispatch them when received on main thread to cwtchNotifier
var _receivePort = ReceivePort();
cwtchIsolate = await Isolate.spawn(_checkAppbusEvents, _receivePort.sendPort);
_receivePort.listen((message) {
var env = jsonDecode(message);
@ -108,6 +116,14 @@ class CwtchFfi implements Cwtch {
});
}
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground() async {
var reconnectCwtch = library.lookup<NativeFunction<Void Function()>>("c_ReconnectCwtchForeground");
// ignore: non_constant_identifier_names
final ReconnectCwtchForeground = reconnectCwtch.asFunction<void Function()>();
ReconnectCwtchForeground();
}
// Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events
@override
void dispose() {
@ -122,6 +138,7 @@ class CwtchFfi implements Cwtch {
await for (var value in stream) {
sendPort.send(value);
}
print("checkAppBusEvents finished...");
}
// Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it
@ -133,13 +150,18 @@ class CwtchFfi implements Cwtch {
library = DynamicLibrary.open("libCwtch.so");
}
var getAppbusEventC = library.lookup<NativeFunction<acn_events_function>>("c_GetAppBusEvent");
var getAppbusEventC = library.lookup<NativeFunction<appbus_events_function>>("c_GetAppBusEvent");
// ignore: non_constant_identifier_names
final GetAppbusEvent = getAppbusEventC.asFunction<ACNEventsFn>();
final GetAppbusEvent = getAppbusEventC.asFunction<AppbusEventsFn>();
while (true) {
Pointer<Utf8> result = GetAppbusEvent();
String event = result.toDartString();
if (event.startsWith("{\"EventType\":\"Shutdown\"")) {
print("Shutting down isolate thread: $event");
return;
}
yield event;
}
}
@ -172,50 +194,6 @@ class CwtchFfi implements Cwtch {
LoadProfiles(ut8pass, ut8pass.length);
}
// ignore: non_constant_identifier_names
Future<String> ACNEvents() async {
var acnEventsC = library.lookup<NativeFunction<acn_events_function>>("c_ACNEvents");
// ignore: non_constant_identifier_names
final ACNEvents = acnEventsC.asFunction<ACNEventsFn>();
Pointer<Utf8> result = ACNEvents();
String event = result.toDartString();
return event;
}
// ignore: non_constant_identifier_names
Future<String> ContactEvents() async {
var acnEventsC = library.lookup<NativeFunction<acn_events_function>>("c_ContactEvents");
// ignore: non_constant_identifier_names
final ContactEvents = acnEventsC.asFunction<ACNEventsFn>();
Pointer<Utf8> result = ContactEvents();
String event = result.toDartString();
return event;
}
// ignore: non_constant_identifier_names
Future<String> GetProfiles() async {
var getProfilesC = library.lookup<NativeFunction<get_json_blob_void_function>>("c_GetProfiles");
// ignore: non_constant_identifier_names
final GetProfiles = getProfilesC.asFunction<GetJsonBlobVoidFn>();
Pointer<Utf8> jsonProfilesBytes = GetProfiles();
String jsonProfiles = jsonProfilesBytes.toDartString();
return jsonProfiles;
}
// ignore: non_constant_identifier_names
Future<int> NumMessages(String profile, String handle) async {
var numMessagesC = library.lookup<NativeFunction<get_int_from_str_str_function>>("c_NumMessages");
// ignore: non_constant_identifier_names
final NumMessages = numMessagesC.asFunction<GetIntFromStrStrFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
int num = NumMessages(utf8profile, utf8profile.length, utf8handle, utf8handle.length);
return num;
}
// ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_function>>("c_GetMessage");
@ -228,18 +206,6 @@ class CwtchFfi implements Cwtch {
return jsonMessage;
}
// ignore: non_constant_identifier_names
Future<String> GetMessages(String profile, String handle, int start, int end) async {
var getMessagesC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_int_function>>("c_GetMessages");
// ignore: non_constant_identifier_names
final GetMessages = getMessagesC.asFunction<GetJsonBlobFromStrStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
Pointer<Utf8> jsonMessagesBytes = GetMessages(utf8profile, utf8profile.length, utf8handle, utf8handle.length, start, end);
String jsonMessages = jsonMessagesBytes.toDartString();
return jsonMessages;
}
@override
// ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String json) {
@ -283,17 +249,6 @@ class CwtchFfi implements Cwtch {
BlockContact(u1, u1.length, u2, u2.length);
}
@override
// ignore: non_constant_identifier_names
void DebugResetContact(String profileOnion, String contactHandle) {
var debugResetContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_DebugResetContact");
// ignore: non_constant_identifier_names
final DebugResetContact = debugResetContact.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
DebugResetContact(u1, u1.length, u2, u2.length);
}
@override
// ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) {
@ -373,14 +328,63 @@ class CwtchFfi implements Cwtch {
CreateGroup(u1, u1.length, u2, u2.length, u3, u3.length);
}
@override
// ignore: non_constant_identifier_names
void LeaveConversation(String profileOnion, String handle) {
var leaveConversation = library.lookup<NativeFunction<string_string_to_void_function>>("c_LeaveConversation");
// ignore: non_constant_identifier_names
final LeaveConversation = leaveConversation.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = handle.toNativeUtf8();
LeaveConversation(u1, u1.length, u2, u2.length);
}
@override
// ignore: non_constant_identifier_names
void LeaveGroup(String profileOnion, String groupHandle) {
var leaveGroup = library.lookup<NativeFunction<string_string_to_void_function>>("c_LeaveGroup");
// ignore: non_constant_identifier_names
final RejectInvite = leaveGroup.asFunction<VoidFromStringStringFn>();
final LeaveGroup = leaveGroup.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length);
LeaveGroup(u1, u1.length, u2, u2.length);
}
@override
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
var updateMessageFlagsC = library.lookup<NativeFunction<void_from_string_string_int_int_function>>("c_UpdateMessageFlags");
// ignore: non_constant_identifier_names
final updateMessageFlags = updateMessageFlagsC.asFunction<VoidFromStringStringIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags);
}
@override
// ignore: non_constant_identifier_names
void DeleteProfile(String onion, String currentPassword) {
var deleteprofile = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteProfile");
// ignore: non_constant_identifier_names
final DeleteProfile = deleteprofile.asFunction<VoidFromStringStringFn>();
final u1 = onion.toNativeUtf8();
final u2 = currentPassword.toNativeUtf8();
DeleteProfile(u1, u1.length, u2, u2.length);
}
@override
Future<void> Shutdown() async {
var shutdown = library.lookup<NativeFunction<void_from_void_funtion>>("c_ShutdownCwtch");
// ignore: non_constant_identifier_names
// Shutdown Cwtch + Tor...
final Shutdown = shutdown.asFunction<VoidFromVoidFunction>();
Shutdown();
// Kill our Isolate
cwtchIsolate.kill(priority: Isolate.immediate);
print("Isolate killed");
_receivePort.close();
print("Receive Port Closed");
}
}

View File

@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/config.dart';
import 'package:flutter/services.dart';
@ -51,7 +50,13 @@ class CwtchGomobile implements Cwtch {
}
String torPath = path.join(await androidLibraryDir, "libtor.so");
print("gomobile.dart: Start invokeMethod Start($cwtchDir, $torPath)...");
cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath});
return cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath});
}
@override
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground() async {
cwtchPlatform.invokeMethod("ReconnectCwtchForeground", {});
}
// Handle libcwtch-go events (received via kotlin) and dispatch to the cwtchNotifier
@ -77,24 +82,8 @@ class CwtchGomobile implements Cwtch {
}
// ignore: non_constant_identifier_names
Future<dynamic> ACNEvents() {
return cwtchPlatform.invokeMethod("ACNEvents");
}
// ignore: non_constant_identifier_names
Future<dynamic> ContactEvents() {
return cwtchPlatform.invokeMethod("ContactEvents");
}
// ignore: non_constant_identifier_names
Future<dynamic> GetProfiles() {
print("gomobile.dart: GetProfiles()");
return cwtchPlatform.invokeMethod("GetProfiles");
}
// ignore: non_constant_identifier_names
Future<dynamic> NumMessages(String profile, String handle) {
return cwtchPlatform.invokeMethod("NumMessages", {"profile": profile, "contact": handle});
void DeleteProfile(String onion, String pass) {
cwtchPlatform.invokeMethod("DeleteProfile", {"ProfileOnion": onion, "pass": pass});
}
// ignore: non_constant_identifier_names
@ -103,11 +92,6 @@ class CwtchGomobile implements Cwtch {
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index});
}
// ignore: non_constant_identifier_names
Future<dynamic> GetMessages(String profile, String handle, int start, int end) {
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "start": start, "end": end});
}
@override
// ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String jsonEvent) {
@ -135,12 +119,6 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "handle": contactHandle});
}
@override
// ignore: non_constant_identifier_names
void DebugResetContact(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("DebugResetContact", {"ProfileOnion": profileOnion, "handle": contactHandle});
}
@override
// ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) {
@ -174,7 +152,7 @@ class CwtchGomobile implements Cwtch {
@override
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "handle": groupHandle});
cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "groupHandle": groupHandle});
}
@override
@ -185,6 +163,24 @@ class CwtchGomobile implements Cwtch {
@override
// ignore: non_constant_identifier_names
void LeaveGroup(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "handle": groupHandle});
cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "groupHandle": groupHandle});
}
@override
// ignore: non_constant_identifier_names
void LeaveConversation(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("LeaveConversation", {"ProfileOnion": profileOnion, "contactHandle": 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});
}
@override
Future<void> Shutdown() async {
print("gomobile.dart Shutdown");
cwtchPlatform.invokeMethod("Shutdown", {});
}
}

110
lib/cwtch_icons_icons.dart Normal file
View File

@ -0,0 +1,110 @@
/// Flutter icons CwtchIcons
/// Copyright (C) 2021 by Open Privacy Research Society via fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
/// To use this font, place it in your fonts/ directory and include the
/// following in your pubspec.yaml
///
/// flutter:
/// fonts:
/// - family: CwtchIcons
/// fonts:
/// - asset: assets/fonts/CwtchIcons.ttf
///
///
///
import 'package:flutter/widgets.dart';
class CwtchIcons {
CwtchIcons._();
static const _kFontFam = 'CwtchIcons';
static const String? _kFontPkg = null;
static const IconData arrow_back_24px = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData attach_file_24px = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData block_peer = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData block_unknown = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData block_24px = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData brightness_5_24px = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData camera_alt_24px = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData change_language = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData change_theme = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chat_bubble_empty_24px = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chat_bubble_24px = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chat_seetings_24px = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData check_24px = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chevron_left_24px = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData clear_24px = IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData content_copy_24px = IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData create_group = IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData cwtch_knott = IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData dark_mode_24px = IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData delete_24px = IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData dns_24px = IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData drag_indicator_24px = IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData edit_24px = IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData enable_experiments = IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData enable_groups = IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData eye_closed = IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData eye_open = IconData(0xe81a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_24dp = IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_black_24dp_broken = IconData(0xe81c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_black_24dp_brokenhalf = IconData(0xe81d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_black_24dp_malformed = IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_black_24dp_sad = IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData group_add_24px = IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData group_settings_24px = IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData groups_24px = IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData info_24px = IconData(0xe823, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData join_group = IconData(0xe824, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData list_black_24dp = IconData(0xe825, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData lock_open_24px = IconData(0xe826, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData lock_24px = IconData(0xe827, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData maps_ugc_24px = IconData(0xe828, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData menu_24px = IconData(0xe829, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData message_24px = IconData(0xe82a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData mood_24px = IconData(0xe82b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData more_vert_24px = IconData(0xe82c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData negative_heart_24px = IconData(0xe82d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData onion_off = IconData(0xe82e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData onion_on = IconData(0xe82f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData onion_waiting = IconData(0xe830, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData peer_history = IconData(0xe831, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData peer_settings_24px = IconData(0xe832, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData person_add_alt_1_24px = IconData(0xe833, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData person_add_24px = IconData(0xe834, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData person_24px = IconData(0xe835, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData push_pin_black_24dp = IconData(0xe836, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData push_pin_24px = IconData(0xe837, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData search_24px = IconData(0xe838, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData send_24px = IconData(0xe839, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData settings_24px = IconData(0xe83a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData signal_cellular_4_bar_24px = IconData(0xe83b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData signal_cellular_alt_24px = IconData(0xe83c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData signal_cellular_connected_no_internet_4_bar_24px = IconData(0xe83d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData signal_cellular_off_24px = IconData(0xe83e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData swap_horiz_24px = IconData(0xe83f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sync_disabled_24px = IconData(0xe840, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sync_problem_24px = IconData(0xe841, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sync_24px = IconData(0xe842, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData syncing_01 = IconData(0xe843, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData syncing_02 = IconData(0xe844, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData syncing_03 = IconData(0xe845, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData toggle_on_24px = IconData(0xe846, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData vpn_key_24px = IconData(0xe847, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_blocked = IconData(0xe848, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_circle_24px = IconData(0xe849, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_circle_24px_lines = IconData(0xe84a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_circle_24px_lines_thin___blocked = IconData(0xe84b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_circle_24px_user = IconData(0xe84c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData add_circle_24px = IconData(0xe84d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData add_group = IconData(0xe84e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData add_peer = IconData(0xe84f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
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 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

@ -17,6 +17,10 @@ class ErrorHandler extends ChangeNotifier {
bool importBundleError = false;
bool importBundleSuccess = false;
static const String deleteProfileErrorPrefix = "deleteprofile";
bool deleteProfileError = false;
bool deleteProfileSuccess = false;
/// Called by the event bus.
handleUpdate(String error) {
var parts = error.split(".");
@ -30,6 +34,9 @@ class ErrorHandler extends ChangeNotifier {
case importBundleErrorPrefix:
handleImportBundleError(errorType);
break;
case deleteProfileErrorPrefix:
handleDeleteProfileError(errorType);
break;
}
notifyListeners();
@ -69,4 +76,19 @@ class ErrorHandler extends ChangeNotifier {
break;
}
}
handleDeleteProfileError(String errorType) {
// Reset add contact errors
deleteProfileError = false;
deleteProfileSuccess = false;
switch (errorType) {
case successErrorType:
deleteProfileSuccess = true;
break;
default:
deleteProfileError = true;
break;
}
}
}

View File

@ -1,160 +1,193 @@
{
"@@locale": "de",
"acceptGroupBtn": "Annehmen",
"acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen",
"acknowledgedLabel": "bestätigt",
"addListItem": "Liste hinzufügen",
"addListItemBtn": "Element hinzufügen",
"addNewItem": "Ein neues Element zur Liste hinzufügen",
"addNewProfileBtn": "Neues Profil hinzufügen",
"addPeer": "Peer hinzufügen",
"addPeerTab": "Einen Peer hinzufügen",
"addProfileTitle": "Neues Profil hinzufügen",
"addressLabel": "Adresse",
"blockBtn": "Peer blockieren",
"blocked": "Blockiert",
"blockUnknownLabel": "Unbekannte Peers blockieren",
"builddate": "Aufgebaut auf: %2",
"bulletinsBtn": "Meldungen",
"chatBtn": "Chat",
"contactAlreadyExists": "",
"conversationSettings": "",
"copiedClipboardNotification": "in die Zwischenablage kopiert",
"copiedToClipboardNotification": "in die Zwischenablage kopiert",
"copyBtn": "Kopieren",
"couldNotSendMsgError": "Nachricht konnte nicht gesendet werden",
"createGroup": "Gruppe erstellen",
"createGroupBtn": "Anlegen",
"createGroupTab": "Eine Gruppe erstellen",
"createGroupTitle": "Gruppe Anlegen",
"createProfileBtn": "Profil speichern",
"currentPasswordLabel": "derzeitiges Passwort",
"cwtchSettingsTitle": "Cwtch Einstellungen",
"cycleCatsAndroid": "",
"cycleCatsDesktop": "",
"cycleColoursAndroid": "",
"cycleColoursDesktop": "",
"cycleMorphsAndroid": "",
"cycleMorphsDesktop": "",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "Tolle Gruppe",
"defaultProfileName": "Alice",
"defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:",
"deleteBtn": "Löschen",
"deleteConfirmLabel": "Geben Sie LÖSCHEN zur Bestätigung ein",
"deleteConfirmText": "LÖSCHEN",
"deleteProfileBtn": "Profil löschen",
"deleteProfileConfirmBtn": "Profil wirklich löschen",
"descriptionBlockUnknownConnections": "",
"descriptionExperiments": "",
"descriptionExperimentsGroups": "",
"displayNameLabel": "Angezeigter Name",
"dmTooltip": "Klicken, um DM zu senden",
"dontSavePeerHistory": "Peer-Verlauf löschen",
"editProfile": "Profil bearbeiten",
"editProfileTitle": "Profil bearbeiten",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "Geben Sie ein Passwort ein, um Ihre Profile anzuzeigen",
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
"experimentsEnabled": "Experimente aktiviert",
"groupAddr": "Adresse",
"groupName": "Gruppenname",
"groupNameLabel": "Gruppenname",
"invalidImportString": "",
"invitation": "Einladung",
"invitationLabel": "Einladung",
"inviteBtn": "Einladen",
"inviteToGroupLabel": "In die Gruppe einladen",
"joinGroup": "Gruppe beitreten",
"joinGroupTab": "Einer Gruppe beitreten",
"largeTextLabel": "Groß",
"listsBtn": "Listen",
"loadingTor": "Tor wird geladen...",
"localeDe": "Deutsche",
"localeEn": "",
"localeEs": "",
"localeFr": "",
"localeIt": "",
"localePt": "",
"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.",
"networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen",
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
"networkStatusDisconnected": "Vom Internet getrennt, überprüfen Sie Ihre Verbindung",
"networkStatusOnline": "Online",
"newBulletinLabel": "Neue Meldung",
"newConnectionPaneTitle": "Neue Verbindung",
"newGroupBtn": "Neue Gruppe anlegen",
"newProfile": "Neues Profil",
"noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.",
"password": "Passwort",
"password1Label": "Passwort",
"password2Label": "Passwort erneut eingeben",
"passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt",
"passwordErrorEmpty": "Passwort kann nicht leer sein",
"passwordErrorMatch": "Passwörter stimmen nicht überein",
"pasteAddressToAddContact": "Adresse hier hinzufügen, um einen Kontakt aufzunehmen",
"peerAddress": "Adresse",
"peerBlockedMessage": "Peer ist blockiert",
"peerName": "Namen",
"peerNotOnline": "",
"peerOfflineMessage": "Peer ist offline, Nachrichten können derzeit nicht zugestellt werden",
"pendingLabel": "Bestätigung ausstehend",
"postNewBulletinLabel": "Neue Meldung veröffentlichen",
"profileName": "Anzeigename",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"puzzleGameBtn": "Puzzlespiel",
"radioNoPassword": "Unverschlüsselt (kein Passwort)",
"radioUsePassword": "Passwort",
"rejectGroupBtn": "Ablehnen",
"saveBtn": "Speichern",
"savePeerHistory": "Peer-Verlauf speichern",
"savePeerHistoryDescription": "Legt fest, ob ein mit dem Peer verknüpfter Verlauf gelöscht werden soll oder nicht.",
"saveProfileBtn": "Profil speichern",
"search": "Suche...",
"searchList": "",
"server": "Server",
"serverConnectivityConnected": "Server verbunden",
"serverConnectivityDisconnected": "Server getrennt",
"serverInfo": "Server-Informationen",
"serverLabel": "Server",
"serverNotSynced": "",
"serverSynced": "",
"settingInterfaceZoom": "Zoomstufe",
"settingLanguage": "Sprache",
"settingTheme": "Thema",
"smallTextLabel": "Klein",
"successfullAddedContact": "",
"themeDark": "Dunkel",
"themeLight": "Licht",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "Titel...",
"todoPlaceholder": "noch zu erledigen",
"tooltipAddContact": "",
"tooltipOpenSettings": "",
"tooltipUnlockProfiles": "",
"unblockBtn": "Peer entblockieren",
"unlock": "Entsperren",
"update": "",
"version": "Version %1",
"versionBuilddate": "Version: %1 Aufgebaut auf: %2",
"versionTor": "Version %1 mit tor %2",
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
"viewServerInfo": "",
"yourDisplayName": "Ihr Anzeigename",
"yourProfiles": "Ihre Profile",
"yourServers": "Ihre Server",
"zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)"
"@@locale": "de",
"@@last_modified": "2021-06-24T23:32:06+02:00",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",
"shutdownCwtchTooltip": "Shutdown Cwtch",
"malformedMessage": "Malformed message",
"profileDeleteSuccess": "Successfully deleted profile",
"debugLog": "Turn on console debug logging",
"torNetworkStatus": "Tor network status",
"addContactFirst": "Add or pick a contact to begin chatting.",
"createProfileToBegin": "Please create or unlock a profile to begin",
"nickChangeSuccess": "Profile nickname changed successfully",
"addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"sendMessage": "Send Message",
"cancel": "Cancel",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "You sent an invitation for: ",
"contactSuggestion": "This is a contact suggestion for: ",
"rejected": "Rejected!",
"accepted": "Accepted!",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "Adresse hier hinzufügen, um einen Kontakt aufzunehmen",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateMonthsAgo": "Months Ago",
"dateNever": "Never",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateLastYear": "Last Year",
"dateYesterday": "Yesterday",
"dateLastMonth": "Last Month",
"dateWeeksAgo": "Weeks Ago",
"dateDaysAgo": "Days Ago",
"dateHoursAgo": "Hours Ago",
"dateMinutesAgo": "Minutes Ago",
"dateRightNow": "Right Now",
"successfullAddedContact": "Successfully added ",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Experimente aktiviert",
"localeIt": "Italiana",
"localeEs": "Espanol",
"addListItem": "Liste hinzufügen",
"addNewItem": "Ein neues Element zur Liste hinzufügen",
"todoPlaceholder": "noch zu erledigen",
"newConnectionPaneTitle": "Neue Verbindung",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
"networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen",
"networkStatusDisconnected": "Vom Internet getrennt, überprüfen Sie Ihre Verbindung",
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
"loadingTor": "Tor wird geladen...",
"smallTextLabel": "Klein",
"defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:",
"builddate": "Aufgebaut auf: %2",
"version": "Version %1",
"versionTor": "Version %1 mit tor %2",
"themeDark": "Dunkel",
"themeLight": "Licht",
"settingTheme": "Thema",
"largeTextLabel": "Groß",
"settingInterfaceZoom": "Zoomstufe",
"localeDe": "Deutsche",
"localePt": "Portuguesa",
"localeFr": "Frances",
"localeEn": "English",
"settingLanguage": "Sprache",
"blockUnknownLabel": "Unbekannte Peers blockieren",
"zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)",
"versionBuilddate": "Version: %1 Aufgebaut auf: %2",
"cwtchSettingsTitle": "Cwtch Einstellungen",
"unlock": "Entsperren",
"yourServers": "Ihre Server",
"yourProfiles": "Ihre Profile",
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
"password": "Passwort",
"enterProfilePassword": "Geben Sie ein Passwort ein, um Ihre Profile anzuzeigen",
"addNewProfileBtn": "Neues Profil hinzufügen",
"deleteConfirmText": "LÖSCHEN",
"deleteProfileConfirmBtn": "Profil wirklich löschen",
"deleteConfirmLabel": "Geben Sie LÖSCHEN zur Bestätigung ein",
"deleteProfileBtn": "Profil löschen",
"passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt",
"passwordErrorMatch": "Passwörter stimmen nicht überein",
"saveProfileBtn": "Profil speichern",
"createProfileBtn": "Profil speichern",
"passwordErrorEmpty": "Passwort kann nicht leer sein",
"password2Label": "Passwort erneut eingeben",
"password1Label": "Passwort",
"currentPasswordLabel": "derzeitiges Passwort",
"yourDisplayName": "Ihr 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",
"copiedToClipboardNotification": "in die Zwischenablage kopiert",
"copyBtn": "Kopieren",
"editProfile": "Profil bearbeiten",
"newProfile": "Neues Profil",
"defaultProfileName": "Alice",
"profileName": "Anzeigename",
"editProfileTitle": "Profil bearbeiten",
"addProfileTitle": "Neues Profil hinzufügen",
"deleteBtn": "löschen",
"unblockBtn": "Peer entblockieren",
"dontSavePeerHistory": "Peer-Verlauf löschen",
"savePeerHistoryDescription": "Legt fest, ob ein mit dem Peer verknüpfter Verlauf gelöscht werden soll oder nicht.",
"savePeerHistory": "Peer-Verlauf speichern",
"blockBtn": "Peer blockieren",
"saveBtn": "speichern",
"displayNameLabel": "Angezeigter Name",
"addressLabel": "Adresse",
"puzzleGameBtn": "Puzzlespiel",
"bulletinsBtn": "Meldungen",
"listsBtn": "Listen",
"chatBtn": "Chat",
"rejectGroupBtn": "Ablehnen",
"acceptGroupBtn": "Annehmen",
"acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen",
"newGroupBtn": "Neue Gruppe anlegen",
"copiedClipboardNotification": "in die Zwischenablage kopiert",
"peerOfflineMessage": "Peer ist offline, Nachrichten können derzeit nicht zugestellt werden",
"peerBlockedMessage": "Peer ist blockiert",
"pendingLabel": "Bestätigung ausstehend",
"acknowledgedLabel": "bestätigt",
"couldNotSendMsgError": "Nachricht konnte nicht gesendet werden",
"dmTooltip": "Klicken, um DM 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": "Peer is Offline. Applications cannot be used right now.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Einladen",
"inviteToGroupLabel": "In die Gruppe einladen",
"groupNameLabel": "Gruppenname",
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server getrennt",
"serverConnectivityConnected": "Server verbunden",
"serverInfo": "Server-Informationen",
"invitationLabel": "Einladung",
"serverLabel": "Server",
"search": "Suche...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blockiert",
"titlePlaceholder": "Titel...",
"postNewBulletinLabel": "Neue Meldung veröffentlichen",
"newBulletinLabel": "Neue Meldung",
"joinGroup": "Gruppe beitreten",
"createGroup": "Gruppe erstellen",
"addPeer": "Peer hinzufügen",
"groupAddr": "Adresse",
"invitation": "Einladung",
"server": "Server",
"groupName": "Gruppenname",
"peerName": "Namen",
"peerAddress": "Adresse",
"joinGroupTab": "Einer Gruppe beitreten",
"createGroupTab": "Eine Gruppe erstellen",
"addPeerTab": "Einen Peer hinzufügen",
"createGroupBtn": "Anlegen",
"defaultGroupName": "Tolle Gruppe",
"createGroupTitle": "Gruppe Anlegen"
}

View File

@ -1,160 +1,193 @@
{
"@@locale": "en",
"acceptGroupBtn": "Accept",
"acceptGroupInviteLabel": "Do you want to accept the invitation to",
"acknowledgedLabel": "Acknowledged",
"addListItem": "Add a New List Item",
"addListItemBtn": "Add Item",
"addNewItem": "Add a new item to the list",
"addNewProfileBtn": "Add new profile",
"addPeer": "Add Peer",
"addPeerTab": "Add a peer",
"addProfileTitle": "Add new profile",
"addressLabel": "Address",
"blockBtn": "Block Peer",
"blocked": "Blocked",
"blockUnknownLabel": "Block Unknown Peers",
"builddate": "Built on: %2",
"bulletinsBtn": "Bulletins",
"chatBtn": "Chat",
"contactAlreadyExists": "Contact Already Exists",
"conversationSettings": "Conversation Settings",
"copiedClipboardNotification": "Copied to clipboard",
"copiedToClipboardNotification": "Copied to Clipboard",
"copyBtn": "Copy",
"couldNotSendMsgError": "Could not send this message",
"createGroup": "Create group",
"createGroupBtn": "Create",
"createGroupTab": "Create a group",
"createGroupTitle": "Create Group",
"createProfileBtn": "Create Profile",
"currentPasswordLabel": "Current Password",
"cwtchSettingsTitle": "Cwtch Settings",
"cycleCatsAndroid": "Click to cycle category.\\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\\nLong-press to reset.",
"cycleColoursDesktop": "Click to cycle colours.\\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\\nRight-click to reset.",
"dateDaysAgo": "Days Ago",
"dateHoursAgo": "Hours Ago",
"dateLastMonth": "Last Month",
"dateLastYear": "Last Year",
"dateMinutesAgo": "Minutes Ago",
"dateMonthsAgo": "Months Ago",
"dateNever": "Never",
"dateRightNow": "Right Now",
"dateWeeksAgo": "Weeks Ago",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateYesterday": "Yesterday",
"defaultGroupName": "Awesome Group",
"defaultProfileName": "Alice",
"defaultScalingText": "Default size text (scale factor:",
"deleteBtn": "Delete",
"deleteConfirmLabel": "Type DELETE to confirm",
"deleteConfirmText": "DELETE",
"deleteProfileBtn": "Delete Profile",
"deleteProfileConfirmBtn": "Really Delete Profile",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"displayNameLabel": "Display Name",
"dmTooltip": "Click to DM",
"dontSavePeerHistory": "Delete Peer History",
"editProfile": "Edit Profille",
"editProfileTitle": "Edit Profile",
"enableGroups": "Enable Group Chat",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enterProfilePassword": "Enter a password to view your profiles",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
"experimentsEnabled": "Enable Experiments",
"groupAddr": "Address",
"groupName": "Group name",
"groupNameLabel": "Group Name",
"invalidImportString": "Invalid import string",
"invitation": "Invitation",
"invitationLabel": "Invitation",
"inviteBtn": "Invite",
"inviteToGroupLabel": "Invite to group",
"joinGroup": "Join group",
"joinGroupTab": "Join a group",
"largeTextLabel": "Large",
"listsBtn": "Lists",
"loadingTor": "Loading tor...",
"localeDe": "Deutsche",
"localeEn": "English",
"localeEs": "Espanol",
"localeFr": "Frances",
"localeIt": "Italiana",
"localePt": "Portuguesa",
"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.",
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusConnecting": "Connecting to network and peers...",
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
"networkStatusOnline": "Online",
"newBulletinLabel": "New Bulletin",
"newConnectionPaneTitle": "New Connection",
"newGroupBtn": "Create new group",
"newProfile": "New Profile",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"password": "Password",
"password1Label": "Password",
"password2Label": "Reenter password",
"passwordChangeError": "Error changing password: Supplied password rejected",
"passwordErrorEmpty": "Password cannot be empty",
"passwordErrorMatch": "Passwords do not match",
"pasteAddressToAddContact": "Paste a cwtch address here to add a new contact.",
"peerAddress": "Address",
"peerBlockedMessage": "Peer is blocked",
"peerName": "Name",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
"pendingLabel": "Pending",
"postNewBulletinLabel": "Post new bulletin",
"profileName": "Display name",
"profileOnionLabel": "Send this address to peers you want to connect with",
"puzzleGameBtn": "Puzzle Game",
"radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password",
"rejectGroupBtn": "Reject",
"saveBtn": "Save",
"savePeerHistory": "Save Peer History",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
"saveProfileBtn": "Save Profile",
"search": "Search...",
"searchList": "Search List",
"server": "Server",
"serverConnectivityConnected": "Server Connected",
"serverConnectivityDisconnected": "Server Disconnected",
"serverInfo": "Server Information",
"serverLabel": "Server",
"serverNotSynced": "Out of Sync",
"serverSynced": "Synced",
"settingInterfaceZoom": "Zoom level",
"settingLanguage": "Language",
"settingTheme": "Theme",
"smallTextLabel": "Small",
"successfullAddedContact": "Successfully added ",
"themeDark": "Dark",
"themeLight": "Light",
"titleManageContacts": "Manage Contacts",
"titleManageProfiles": "Manage Cwtch Profiles",
"titleManageServers": "Manage Servers",
"titlePlaceholder": "title...",
"todoPlaceholder": "Todo...",
"tooltipAddContact": "Add a new contact",
"tooltipOpenSettings": "Open the settings pane",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"unblockBtn": "Unblock Peer",
"unlock": "Unlock",
"update": "Update",
"version": "Version %1",
"versionBuilddate": "Version: %1 Built on: %2",
"versionTor": "Version %1 with tor %2",
"viewGroupMembershipTooltip": "View Group Membership",
"viewServerInfo": "Server Info",
"yourDisplayName": "Your Display Name",
"yourProfiles": "Your Profiles",
"yourServers": "Your Servers",
"zoomLabel": "Interface zoom (mostly affects text and button sizes)"
"@@locale": "en",
"@@last_modified": "2021-06-24T23:32:06+02:00",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",
"shutdownCwtchTooltip": "Shutdown Cwtch",
"malformedMessage": "Malformed message",
"profileDeleteSuccess": "Successfully deleted profile",
"debugLog": "Turn on console debug logging",
"torNetworkStatus": "Tor network status",
"addContactFirst": "Add or pick a contact to begin chatting.",
"createProfileToBegin": "Please create or unlock a profile to begin",
"nickChangeSuccess": "Profile nickname changed successfully",
"addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"sendMessage": "Send Message",
"cancel": "Cancel",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "You sent an invitation for: ",
"contactSuggestion": "This is a contact suggestion for: ",
"rejected": "Rejected!",
"accepted": "Accepted!",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateMonthsAgo": "Months Ago",
"dateNever": "Never",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateLastYear": "Last Year",
"dateYesterday": "Yesterday",
"dateLastMonth": "Last Month",
"dateWeeksAgo": "Weeks Ago",
"dateDaysAgo": "Days Ago",
"dateHoursAgo": "Hours Ago",
"dateMinutesAgo": "Minutes Ago",
"dateRightNow": "Right Now",
"successfullAddedContact": "Successfully added ",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Enable Experiments",
"localeIt": "Italiana",
"localeEs": "Espanol",
"addListItem": "Add a New List Item",
"addNewItem": "Add a new item to the list",
"todoPlaceholder": "Todo...",
"newConnectionPaneTitle": "New Connection",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Connecting to network and peers...",
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
"viewGroupMembershipTooltip": "View Group Membership",
"loadingTor": "Loading tor...",
"smallTextLabel": "Small",
"defaultScalingText": "Default size text (scale factor:",
"builddate": "Built on: %2",
"version": "Version %1",
"versionTor": "Version %1 with tor %2",
"themeDark": "Dark",
"themeLight": "Light",
"settingTheme": "Theme",
"largeTextLabel": "Large",
"settingInterfaceZoom": "Zoom level",
"localeDe": "Deutsche",
"localePt": "Portuguesa",
"localeFr": "Frances",
"localeEn": "English",
"settingLanguage": "Language",
"blockUnknownLabel": "Block Unknown Peers",
"zoomLabel": "Interface zoom (mostly affects text and button sizes)",
"versionBuilddate": "Version: %1 Built on: %2",
"cwtchSettingsTitle": "Cwtch Settings",
"unlock": "Unlock",
"yourServers": "Your Servers",
"yourProfiles": "Your Profiles",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
"password": "Password",
"enterProfilePassword": "Enter a password to view your profiles",
"addNewProfileBtn": "Add new profile",
"deleteConfirmText": "DELETE",
"deleteProfileConfirmBtn": "Really Delete Profile",
"deleteConfirmLabel": "Type DELETE to confirm",
"deleteProfileBtn": "Delete Profile",
"passwordChangeError": "Error changing password: Supplied password rejected",
"passwordErrorMatch": "Passwords do not match",
"saveProfileBtn": "Save Profile",
"createProfileBtn": "Create Profile",
"passwordErrorEmpty": "Password cannot be empty",
"password2Label": "Reenter password",
"password1Label": "Password",
"currentPasswordLabel": "Current Password",
"yourDisplayName": "Your Display Name",
"profileOnionLabel": "Send this address to peers you want to connect with",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copied to Clipboard",
"copyBtn": "Copy",
"editProfile": "Edit Profille",
"newProfile": "New Profile",
"defaultProfileName": "Alice",
"profileName": "Display name",
"editProfileTitle": "Edit Profile",
"addProfileTitle": "Add new profile",
"deleteBtn": "Delete",
"unblockBtn": "Unblock Peer",
"dontSavePeerHistory": "Delete Peer History",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
"savePeerHistory": "Save Peer History",
"blockBtn": "Block Peer",
"saveBtn": "Save",
"displayNameLabel": "Display Name",
"addressLabel": "Address",
"puzzleGameBtn": "Puzzle Game",
"bulletinsBtn": "Bulletins",
"listsBtn": "Lists",
"chatBtn": "Chat",
"rejectGroupBtn": "Reject",
"acceptGroupBtn": "Accept",
"acceptGroupInviteLabel": "Do you want to accept the invitation to",
"newGroupBtn": "Create new group",
"copiedClipboardNotification": "Copied to clipboard",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
"peerBlockedMessage": "Peer is blocked",
"pendingLabel": "Pending",
"acknowledgedLabel": "Acknowledged",
"couldNotSendMsgError": "Could not send this message",
"dmTooltip": "Click to DM",
"membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.",
"addListItemBtn": "Add Item",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Invite",
"inviteToGroupLabel": "Invite to group",
"groupNameLabel": "Group Name",
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected",
"serverInfo": "Server Information",
"invitationLabel": "Invitation",
"serverLabel": "Server",
"search": "Search...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked",
"titlePlaceholder": "title...",
"postNewBulletinLabel": "Post new bulletin",
"newBulletinLabel": "New Bulletin",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
"groupName": "Group name",
"peerName": "Name",
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Create",
"defaultGroupName": "Awesome Group",
"createGroupTitle": "Create Group"
}

View File

@ -1,160 +1,193 @@
{
"@@locale": "es",
"acceptGroupBtn": "Aceptar",
"acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ",
"acknowledgedLabel": "Reconocido",
"addListItem": "Añadir un nuevo elemento a la lista",
"addListItemBtn": "Agregar artículo",
"addNewItem": "Añadir un nuevo elemento a la lista",
"addNewProfileBtn": "Agregar nuevo perfil",
"addPeer": "Agregar Contacto",
"addPeerTab": "Agregar Contacto",
"addProfileTitle": "Agregar nuevo perfil",
"addressLabel": "Dirección",
"blockBtn": "Bloquear contacto",
"blocked": "Bloqueado",
"blockUnknownLabel": "Bloquear conexiones desconocidas",
"builddate": "Basado en: %2",
"bulletinsBtn": "Boletines",
"chatBtn": "Chat",
"contactAlreadyExists": "",
"conversationSettings": "",
"copiedClipboardNotification": "Copiado al portapapeles",
"copiedToClipboardNotification": "Copiado al portapapeles",
"copyBtn": "Copiar",
"couldNotSendMsgError": "No se pudo enviar este mensaje",
"createGroup": "Crear perfil",
"createGroupBtn": "Crear",
"createGroupTab": "Crear un grupo",
"createGroupTitle": "Crear un grupo",
"createProfileBtn": "Crear perfil",
"currentPasswordLabel": "Contraseña actual",
"cwtchSettingsTitle": "Configuración de Cwtch",
"cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.",
"cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.",
"cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.",
"cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "El Grupo Asombroso",
"defaultProfileName": "Alicia",
"defaultScalingText": "Tamaño predeterminado de texto (factor de escala:",
"deleteBtn": "Eliminar",
"deleteConfirmLabel": "Escribe ELIMINAR para confirmar",
"deleteConfirmText": "ELIMINAR",
"deleteProfileBtn": "Eliminar Perfil",
"deleteProfileConfirmBtn": "Confirmar eliminar perfil",
"descriptionBlockUnknownConnections": "",
"descriptionExperiments": "",
"descriptionExperimentsGroups": "",
"displayNameLabel": "Nombre de Usuario",
"dmTooltip": "Haz clic para enviar mensaje directo",
"dontSavePeerHistory": "Eliminar historial de contacto",
"editProfile": "Editar perfil",
"editProfileTitle": "Editar perfil",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
"error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña",
"experimentsEnabled": "Experimentos habilitados",
"groupAddr": "Dirección",
"groupName": "Nombre del grupo",
"groupNameLabel": "Nombre del grupo",
"invalidImportString": "",
"invitation": "Invitación",
"invitationLabel": "Invitación",
"inviteBtn": "Invitar",
"inviteToGroupLabel": "Invitar al grupo",
"joinGroup": "Únete al grupo",
"joinGroupTab": "Únete a un grupo",
"largeTextLabel": "Grande",
"listsBtn": "Listas",
"loadingTor": "Cargando tor...",
"localeDe": "Alemán",
"localeEn": "Inglés",
"localeEs": "Español",
"localeFr": "Francés",
"localeIt": "Italiano",
"localePt": "Portugués",
"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",
"networkStatusAttemptingTor": "Intentando conectarse a la red Tor",
"networkStatusConnecting": "Conectando a la red y a los contactos...",
"networkStatusDisconnected": "Sin conexión, comprueba tu conexión",
"networkStatusOnline": "En línea",
"newBulletinLabel": "Nuevo Boletín",
"newConnectionPaneTitle": "Nueva conexión",
"newGroupBtn": "Crear un nuevo grupo de chat",
"newProfile": "Nuevo perfil",
"noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados",
"password": "Contraseña",
"password1Label": "Contraseña",
"password2Label": "Vuelve a ingresar tu contraseña",
"passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada",
"passwordErrorEmpty": "El campo de contraseña no puede estar vacío",
"passwordErrorMatch": "Las contraseñas no coinciden",
"pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...",
"peerAddress": "Dirección",
"peerBlockedMessage": "Contacto bloqueado",
"peerName": "Nombre",
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
"pendingLabel": "Pendiente",
"postNewBulletinLabel": "Publicar nuevo boletín",
"profileName": "Nombre de Usuario",
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
"puzzleGameBtn": "Juego de rompecabezas",
"radioNoPassword": "Sin cifrado (sin contraseña)",
"radioUsePassword": "Contraseña",
"rejectGroupBtn": "Rechazar",
"saveBtn": "Guardar",
"savePeerHistory": "Guardar el historial con contacto",
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
"saveProfileBtn": "Guardar perfil",
"search": "Búsqueda...",
"searchList": "Buscar en la lista",
"server": "Servidor",
"serverConnectivityConnected": "Servidor conectado",
"serverConnectivityDisconnected": "Servidor desconectado",
"serverInfo": "Información del servidor",
"serverLabel": "Servidor",
"serverNotSynced": "Fuera de sincronización con el servidor",
"serverSynced": "Sincronizado",
"settingInterfaceZoom": "Nivel de zoom",
"settingLanguage": "Idioma",
"settingTheme": "Tema",
"smallTextLabel": "Pequeño",
"successfullAddedContact": "",
"themeDark": "Oscuro",
"themeLight": "Claro",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "título...",
"todoPlaceholder": "Por hacer...",
"tooltipAddContact": "",
"tooltipOpenSettings": "",
"tooltipUnlockProfiles": "",
"unblockBtn": "Desbloquear contacto",
"unlock": "Desbloquear",
"update": "Actualizar",
"version": "Versión %1",
"versionBuilddate": "Versión: %1 Basado en %2",
"versionTor": "Versión %1 con tor %2",
"viewGroupMembershipTooltip": "Ver membresía del grupo",
"viewServerInfo": "Información del servidor",
"yourDisplayName": "Tu nombre de usuario",
"yourProfiles": "Tus perfiles",
"yourServers": "Tus servidores",
"zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)"
"@@locale": "es",
"@@last_modified": "2021-06-24T23:32:06+02:00",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Fuera de sincronización con el servidor",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",
"shutdownCwtchTooltip": "Shutdown Cwtch",
"malformedMessage": "Malformed message",
"profileDeleteSuccess": "Successfully deleted profile",
"debugLog": "Turn on console debug logging",
"torNetworkStatus": "Tor network status",
"addContactFirst": "Add or pick a contact to begin chatting.",
"createProfileToBegin": "Please create or unlock a profile to begin",
"nickChangeSuccess": "Profile nickname changed successfully",
"addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"sendMessage": "Send Message",
"cancel": "Cancel",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "You sent an invitation for: ",
"contactSuggestion": "This is a contact suggestion for: ",
"rejected": "Rejected!",
"accepted": "Accepted!",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateMonthsAgo": "Months Ago",
"dateNever": "Never",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateLastYear": "Last Year",
"dateYesterday": "Yesterday",
"dateLastMonth": "Last Month",
"dateWeeksAgo": "Weeks Ago",
"dateDaysAgo": "Days Ago",
"dateHoursAgo": "Hours Ago",
"dateMinutesAgo": "Minutes Ago",
"dateRightNow": "Right Now",
"successfullAddedContact": "Successfully added ",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Experimentos habilitados",
"localeIt": "Italiano",
"localeEs": "Español",
"addListItem": "Añadir un nuevo elemento a la lista",
"addNewItem": "Añadir un nuevo elemento a la lista",
"todoPlaceholder": "Por hacer...",
"newConnectionPaneTitle": "Nueva conexión",
"networkStatusOnline": "En línea",
"networkStatusConnecting": "Conectando a la red y a los contactos...",
"networkStatusAttemptingTor": "Intentando conectarse a la red Tor",
"networkStatusDisconnected": "Sin conexión, comprueba tu conexión",
"viewGroupMembershipTooltip": "Ver membresía del grupo",
"loadingTor": "Cargando tor...",
"smallTextLabel": "Pequeño",
"defaultScalingText": "Tamaño predeterminado de texto (factor de escala:",
"builddate": "Basado en: %2",
"version": "Versión %1",
"versionTor": "Versión %1 con tor %2",
"themeDark": "Oscuro",
"themeLight": "Claro",
"settingTheme": "Tema",
"largeTextLabel": "Grande",
"settingInterfaceZoom": "Nivel de zoom",
"localeDe": "Alemán",
"localePt": "Portugués",
"localeFr": "Francés",
"localeEn": "Inglés",
"settingLanguage": "Idioma",
"blockUnknownLabel": "Bloquear conexiones desconocidas",
"zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)",
"versionBuilddate": "Versión: %1 Basado en %2",
"cwtchSettingsTitle": "Configuración de Cwtch",
"unlock": "Desbloquear",
"yourServers": "Tus servidores",
"yourProfiles": "Tus perfiles",
"error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña",
"password": "Contraseña",
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
"addNewProfileBtn": "Agregar nuevo perfil",
"deleteConfirmText": "ELIMINAR",
"deleteProfileConfirmBtn": "Confirmar eliminar perfil",
"deleteConfirmLabel": "Escribe ELIMINAR para confirmar",
"deleteProfileBtn": "Eliminar Perfil",
"passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada",
"passwordErrorMatch": "Las contraseñas no coinciden",
"saveProfileBtn": "Guardar perfil",
"createProfileBtn": "Crear perfil",
"passwordErrorEmpty": "El campo de contraseña no puede estar vacío",
"password2Label": "Vuelve a ingresar tu contraseña",
"password1Label": "Contraseña",
"currentPasswordLabel": "Contraseña actual",
"yourDisplayName": "Tu nombre de usuario",
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
"noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados",
"radioNoPassword": "Sin cifrado (sin contraseña)",
"radioUsePassword": "Contraseña",
"copiedToClipboardNotification": "Copiado al portapapeles",
"copyBtn": "Copiar",
"editProfile": "Editar perfil",
"newProfile": "Nuevo perfil",
"defaultProfileName": "Alicia",
"profileName": "Nombre de Usuario",
"editProfileTitle": "Editar perfil",
"addProfileTitle": "Agregar nuevo perfil",
"deleteBtn": "Eliminar",
"unblockBtn": "Desbloquear contacto",
"dontSavePeerHistory": "Eliminar historial de contacto",
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
"savePeerHistory": "Guardar el historial con contacto",
"blockBtn": "Bloquear contacto",
"saveBtn": "Guardar",
"displayNameLabel": "Nombre de Usuario",
"addressLabel": "Dirección",
"puzzleGameBtn": "Juego de rompecabezas",
"bulletinsBtn": "Boletines",
"listsBtn": "Listas",
"chatBtn": "Chat",
"rejectGroupBtn": "Rechazar",
"acceptGroupBtn": "Aceptar",
"acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ",
"newGroupBtn": "Crear un nuevo grupo de chat",
"copiedClipboardNotification": "Copiado al portapapeles",
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
"peerBlockedMessage": "Contacto bloqueado",
"pendingLabel": "Pendiente",
"acknowledgedLabel": "Reconocido",
"couldNotSendMsgError": "No se pudo enviar este mensaje",
"dmTooltip": "Haz clic para enviar mensaje directo",
"membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo",
"addListItemBtn": "Agregar artículo",
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
"searchList": "Buscar en la lista",
"update": "Actualizar",
"inviteBtn": "Invitar",
"inviteToGroupLabel": "Invitar al grupo",
"groupNameLabel": "Nombre del grupo",
"viewServerInfo": "Información del servidor",
"serverSynced": "Sincronizado",
"serverConnectivityDisconnected": "Servidor desconectado",
"serverConnectivityConnected": "Servidor conectado",
"serverInfo": "Información del servidor",
"invitationLabel": "Invitación",
"serverLabel": "Servidor",
"search": "Búsqueda...",
"cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.",
"cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.",
"cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.",
"blocked": "Bloqueado",
"titlePlaceholder": "título...",
"postNewBulletinLabel": "Publicar nuevo boletín",
"newBulletinLabel": "Nuevo Boletín",
"joinGroup": "Únete al grupo",
"createGroup": "Crear perfil",
"addPeer": "Agregar Contacto",
"groupAddr": "Dirección",
"invitation": "Invitación",
"server": "Servidor",
"groupName": "Nombre del grupo",
"peerName": "Nombre",
"peerAddress": "Dirección",
"joinGroupTab": "Únete a un grupo",
"createGroupTab": "Crear un grupo",
"addPeerTab": "Agregar Contacto",
"createGroupBtn": "Crear",
"defaultGroupName": "El Grupo Asombroso",
"createGroupTitle": "Crear un grupo"
}

View File

@ -1,160 +1,193 @@
{
"@@locale": "fr",
"acceptGroupBtn": "Accepter",
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe",
"acknowledgedLabel": "Confirmé",
"addListItem": "Ajouter un nouvel élément",
"addListItemBtn": "",
"addNewItem": "Ajouter un nouvel élément à la liste",
"addNewProfileBtn": "",
"addPeer": "",
"addPeerTab": "",
"addProfileTitle": "",
"addressLabel": "Adresse",
"blockBtn": "",
"blocked": "",
"blockUnknownLabel": "",
"builddate": "",
"bulletinsBtn": "Bulletins",
"chatBtn": "Discuter",
"contactAlreadyExists": "",
"conversationSettings": "",
"copiedClipboardNotification": "Copié dans le presse-papier",
"copiedToClipboardNotification": "Copié dans le presse-papier",
"copyBtn": "Copier",
"couldNotSendMsgError": "Impossible d'envoyer ce message",
"createGroup": "",
"createGroupBtn": "Créer",
"createGroupTab": "",
"createGroupTitle": "Créer un groupe",
"createProfileBtn": "",
"currentPasswordLabel": "",
"cwtchSettingsTitle": "Préférences Cwtch",
"cycleCatsAndroid": "",
"cycleCatsDesktop": "",
"cycleColoursAndroid": "",
"cycleColoursDesktop": "",
"cycleMorphsAndroid": "",
"cycleMorphsDesktop": "",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "Un super groupe",
"defaultProfileName": "",
"defaultScalingText": "Taille par défaut du texte (échelle:",
"deleteBtn": "Effacer",
"deleteConfirmLabel": "",
"deleteConfirmText": "",
"deleteProfileBtn": "",
"deleteProfileConfirmBtn": "",
"descriptionBlockUnknownConnections": "",
"descriptionExperiments": "",
"descriptionExperimentsGroups": "",
"displayNameLabel": "Pseudo",
"dmTooltip": "Envoyer un message privé",
"dontSavePeerHistory": "",
"editProfile": "",
"editProfileTitle": "",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "",
"error0ProfilesLoadedForPassword": "",
"experimentsEnabled": "",
"groupAddr": "",
"groupName": "",
"groupNameLabel": "Nom du groupe",
"invalidImportString": "",
"invitation": "",
"invitationLabel": "Invitation",
"inviteBtn": "Invitation",
"inviteToGroupLabel": "Inviter quelqu'un",
"joinGroup": "",
"joinGroupTab": "",
"largeTextLabel": "Large",
"listsBtn": "Listes",
"loadingTor": "",
"localeDe": "",
"localeEn": "",
"localeEs": "",
"localeFr": "",
"localeIt": "",
"localePt": "",
"membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être representatives de l'ensemble des membres du groupe.",
"networkStatusAttemptingTor": "",
"networkStatusConnecting": "",
"networkStatusDisconnected": "",
"networkStatusOnline": "",
"newBulletinLabel": "Nouveau bulletin",
"newConnectionPaneTitle": "",
"newGroupBtn": "Créer un nouveau groupe",
"newProfile": "",
"noPasswordWarning": "",
"password": "",
"password1Label": "",
"password2Label": "",
"passwordChangeError": "",
"passwordErrorEmpty": "",
"passwordErrorMatch": "",
"pasteAddressToAddContact": "... coller une adresse ici pour ajouter un contact...",
"peerAddress": "",
"peerBlockedMessage": "",
"peerName": "",
"peerNotOnline": "",
"peerOfflineMessage": "",
"pendingLabel": "En attente",
"postNewBulletinLabel": "Envoyer un nouveau bulletin",
"profileName": "",
"profileOnionLabel": "",
"puzzleGameBtn": "Puzzle",
"radioNoPassword": "",
"radioUsePassword": "",
"rejectGroupBtn": "Refuser",
"saveBtn": "Sauvegarder",
"savePeerHistory": "",
"savePeerHistoryDescription": "",
"saveProfileBtn": "",
"search": "",
"searchList": "",
"server": "",
"serverConnectivityConnected": "",
"serverConnectivityDisconnected": "",
"serverInfo": "",
"serverLabel": "Serveur",
"serverNotSynced": "",
"serverSynced": "",
"settingInterfaceZoom": "",
"settingLanguage": "",
"settingTheme": "",
"smallTextLabel": "Petit",
"successfullAddedContact": "",
"themeDark": "",
"themeLight": "",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "titre...",
"todoPlaceholder": "A faire...",
"tooltipAddContact": "",
"tooltipOpenSettings": "",
"tooltipUnlockProfiles": "",
"unblockBtn": "",
"unlock": "",
"update": "",
"version": "",
"versionBuilddate": "",
"versionTor": "",
"viewGroupMembershipTooltip": "",
"viewServerInfo": "",
"yourDisplayName": "",
"yourProfiles": "",
"yourServers": "",
"zoomLabel": "Interface zoom (essentiellement la taille du texte et des composants de l'interface)"
"@@locale": "fr",
"@@last_modified": "2021-06-24T23:32:06+02:00",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",
"shutdownCwtchTooltip": "Shutdown Cwtch",
"malformedMessage": "Malformed message",
"profileDeleteSuccess": "Successfully deleted profile",
"debugLog": "Turn on console debug logging",
"torNetworkStatus": "Tor network status",
"addContactFirst": "Add or pick a contact to begin chatting.",
"createProfileToBegin": "Please create or unlock a profile to begin",
"nickChangeSuccess": "Profile nickname changed successfully",
"addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"sendMessage": "Send Message",
"cancel": "Cancel",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "You sent an invitation for: ",
"contactSuggestion": "This is a contact suggestion for: ",
"rejected": "Rejected!",
"accepted": "Accepted!",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "... coller une adresse ici pour ajouter un contact...",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateMonthsAgo": "Months Ago",
"dateNever": "Never",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateLastYear": "Last Year",
"dateYesterday": "Yesterday",
"dateLastMonth": "Last Month",
"dateWeeksAgo": "Weeks Ago",
"dateDaysAgo": "Days Ago",
"dateHoursAgo": "Hours Ago",
"dateMinutesAgo": "Minutes Ago",
"dateRightNow": "Right Now",
"successfullAddedContact": "Successfully added ",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Enable Experiments",
"localeIt": "Italiana",
"localeEs": "Espanol",
"addListItem": "Ajouter un nouvel élément",
"addNewItem": "Ajouter un nouvel élément à la liste",
"todoPlaceholder": "A faire...",
"newConnectionPaneTitle": "New Connection",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Connecting to network and peers...",
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
"viewGroupMembershipTooltip": "View Group Membership",
"loadingTor": "Loading tor...",
"smallTextLabel": "Petit",
"defaultScalingText": "Taille par défaut du texte (échelle:",
"builddate": "Built on: %2",
"version": "Version %1",
"versionTor": "Version %1 with tor %2",
"themeDark": "Dark",
"themeLight": "Light",
"settingTheme": "Theme",
"largeTextLabel": "Large",
"settingInterfaceZoom": "Zoom level",
"localeDe": "Deutsche",
"localePt": "Portuguesa",
"localeFr": "Frances",
"localeEn": "English",
"settingLanguage": "Language",
"blockUnknownLabel": "Block Unknown Peers",
"zoomLabel": "Interface zoom (essentiellement la taille du texte et des composants de l'interface)",
"versionBuilddate": "Version: %1 Built on: %2",
"cwtchSettingsTitle": "Préférences Cwtch",
"unlock": "Unlock",
"yourServers": "Your Servers",
"yourProfiles": "Your Profiles",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
"password": "Password",
"enterProfilePassword": "Enter a password to view your profiles",
"addNewProfileBtn": "Add new profile",
"deleteConfirmText": "DELETE",
"deleteProfileConfirmBtn": "Really Delete Profile",
"deleteConfirmLabel": "Type DELETE to confirm",
"deleteProfileBtn": "Delete Profile",
"passwordChangeError": "Error changing password: Supplied password rejected",
"passwordErrorMatch": "Passwords do not match",
"saveProfileBtn": "Save Profile",
"createProfileBtn": "Create Profile",
"passwordErrorEmpty": "Password cannot be empty",
"password2Label": "Reenter password",
"password1Label": "Password",
"currentPasswordLabel": "Current Password",
"yourDisplayName": "Your Display Name",
"profileOnionLabel": "Send this address to peers you want to connect with",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copié dans le presse-papier",
"copyBtn": "Copier",
"editProfile": "Edit Profille",
"newProfile": "New Profile",
"defaultProfileName": "Alice",
"profileName": "Display name",
"editProfileTitle": "Edit Profile",
"addProfileTitle": "Add new profile",
"deleteBtn": "Effacer",
"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": "Sauvegarder",
"displayNameLabel": "Pseudo",
"addressLabel": "Adresse",
"puzzleGameBtn": "Puzzle",
"bulletinsBtn": "Bulletins",
"listsBtn": "Listes",
"chatBtn": "Discuter",
"rejectGroupBtn": "Refuser",
"acceptGroupBtn": "Accepter",
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe",
"newGroupBtn": "Créer un nouveau groupe",
"copiedClipboardNotification": "Copié dans le presse-papier",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
"peerBlockedMessage": "Peer is blocked",
"pendingLabel": "En attente",
"acknowledgedLabel": "Confirmé",
"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 representatives de l'ensemble des membres du groupe.",
"addListItemBtn": "Add Item",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
"searchList": "Search List",
"update": "Update",
"inviteBtn": "Invitation",
"inviteToGroupLabel": "Inviter quelqu'un",
"groupNameLabel": "Nom du groupe",
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected",
"serverInfo": "Server Information",
"invitationLabel": "Invitation",
"serverLabel": "Serveur",
"search": "Search...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked",
"titlePlaceholder": "titre...",
"postNewBulletinLabel": "Envoyer un nouveau bulletin",
"newBulletinLabel": "Nouveau bulletin",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
"groupName": "Group name",
"peerName": "Name",
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Créer",
"defaultGroupName": "Un super groupe",
"createGroupTitle": "Créer un groupe"
}

View File

@ -1,160 +1,193 @@
{
"@@locale": "it",
"acceptGroupBtn": "Accetta",
"acceptGroupInviteLabel": "Vuoi accettare l'invito a",
"acknowledgedLabel": "Riconosciuto",
"addListItem": "Aggiungi un nuovo elemento alla lista",
"addListItemBtn": "Aggiungi elemento",
"addNewItem": "Aggiungi un nuovo elemento alla lista",
"addNewProfileBtn": "Aggiungi nuovo profilo",
"addPeer": "Aggiungi peer",
"addPeerTab": "Aggiungi un peer",
"addProfileTitle": "Aggiungi nuovo profilo",
"addressLabel": "Indirizzo",
"blockBtn": "Blocca il peer",
"blocked": "Bloccato",
"blockUnknownLabel": "Blocca peer sconosciuti",
"builddate": "Costruito il: %2",
"bulletinsBtn": "Bollettini",
"chatBtn": "Chat",
"contactAlreadyExists": "",
"conversationSettings": "",
"copiedClipboardNotification": "Copiato negli Appunti",
"copiedToClipboardNotification": "Copiato negli Appunti",
"copyBtn": "Copia",
"couldNotSendMsgError": "Impossibile inviare questo messaggio",
"createGroup": "Crea un gruppo",
"createGroupBtn": "Crea",
"createGroupTab": "Crea un gruppo",
"createGroupTitle": "Crea un gruppo",
"createProfileBtn": "Crea un profilo",
"currentPasswordLabel": "Password corrente",
"cwtchSettingsTitle": "Impostazioni di Cwtch",
"cycleCatsAndroid": "Fare clic per scorrere le categorie.\\nPressione lunga per resettare.",
"cycleCatsDesktop": "Fare clic per scorrere le categorie.\\nCliccare con il tasto destro per resettare.",
"cycleColoursAndroid": "Fare clic per scorrere i colori.\\nPressione lunga per resettare.",
"cycleColoursDesktop": "Fare clic per scorrere i colori.\\nCliccare con il tasto destro per resettare.",
"cycleMorphsAndroid": "Fare clic per scorrere i morph.\\nPressione lunga per resettare.",
"cycleMorphsDesktop": "Fare clic per scorrere i morph.\\nCliccare con il tasto destro per resettare.",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "Gruppo fantastico",
"defaultProfileName": "Alice",
"defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:",
"deleteBtn": "Elimina",
"deleteConfirmLabel": "Digita ELIMINA per confermare",
"deleteConfirmText": "ELIMINA",
"deleteProfileBtn": "Elimina profilo",
"deleteProfileConfirmBtn": "Elimina realmente il profilo",
"descriptionBlockUnknownConnections": "",
"descriptionExperiments": "",
"descriptionExperimentsGroups": "",
"displayNameLabel": "Nome visualizzato",
"dmTooltip": "Clicca per inviare un Messagio Diretto",
"dontSavePeerHistory": "Elimina cronologia dei peer",
"editProfile": "Modifica profilo",
"editProfileTitle": "Modifica profilo",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili",
"error0ProfilesLoadedForPassword": "0 profili caricati con quella password",
"experimentsEnabled": "Esperimenti abilitati",
"groupAddr": "Indirizzo",
"groupName": "Nome del gruppo",
"groupNameLabel": "Nome del gruppo",
"invalidImportString": "",
"invitation": "Invito",
"invitationLabel": "Invito",
"inviteBtn": "Invitare",
"inviteToGroupLabel": "Invitare nel gruppo",
"joinGroup": "Unisciti al gruppo",
"joinGroupTab": "Unisciti a un gruppo",
"largeTextLabel": "Grande",
"listsBtn": "Liste",
"loadingTor": "Caricamento di tor...",
"localeDe": "Tedesco",
"localeEn": "Inglese",
"localeEs": "Spagnolo",
"localeFr": "Francese",
"localeIt": "Italiano",
"localePt": "Portoghese",
"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.",
"networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor",
"networkStatusConnecting": "Connessione alla rete e ai peer ...",
"networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione",
"networkStatusOnline": "Online",
"newBulletinLabel": "Nuovo bollettino",
"newConnectionPaneTitle": "Nuova connessione",
"newGroupBtn": "Crea un nuovo gruppo",
"newProfile": "Nuovo profilo",
"noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati",
"password": "Password",
"password1Label": "Password",
"password2Label": "Reinserire la password",
"passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata",
"passwordErrorEmpty": "La password non può essere vuota",
"passwordErrorMatch": "Le password non corrispondono",
"pasteAddressToAddContact": "... incolla qui un indirizzo per aggiungere un contatto...",
"peerAddress": "Indirizzo",
"peerBlockedMessage": "Il peer è bloccato",
"peerName": "Nome",
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
"peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
"pendingLabel": "In corso",
"postNewBulletinLabel": "Pubblica un nuovo bollettino",
"profileName": "Nome visualizzato",
"profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
"puzzleGameBtn": "Gioco di puzzle",
"radioNoPassword": "Non criptato (senza password)",
"radioUsePassword": "Password",
"rejectGroupBtn": "Rifiuta",
"saveBtn": "Salva",
"savePeerHistory": "Salva cronologia peer",
"savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
"saveProfileBtn": "Salva il profilo",
"search": "Ricerca...",
"searchList": "Cerca nella lista",
"server": "Server",
"serverConnectivityConnected": "Server connesso",
"serverConnectivityDisconnected": "Server disconnesso",
"serverInfo": "Informazioni sul server",
"serverLabel": "Server",
"serverNotSynced": "Non sincronizzato",
"serverSynced": "Sincronizzato",
"settingInterfaceZoom": "Livello di zoom",
"settingLanguage": "Lingua",
"settingTheme": "Tema",
"smallTextLabel": "Piccolo",
"successfullAddedContact": "",
"themeDark": "Scuro",
"themeLight": "Chiaro",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "titolo...",
"todoPlaceholder": "Da fare...",
"tooltipAddContact": "",
"tooltipOpenSettings": "",
"tooltipUnlockProfiles": "",
"unblockBtn": "Sblocca il peer",
"unlock": "Sblocca",
"update": "Aggiornamento",
"version": "Versione %1",
"versionBuilddate": "Versione: %1 Costruito il: %2",
"versionTor": "Versione %1 con tor %2",
"viewGroupMembershipTooltip": "Visualizza i membri del gruppo",
"viewServerInfo": "Informazioni sul server",
"yourDisplayName": "Il tuo nome visualizzato",
"yourProfiles": "I tuoi profili",
"yourServers": "I tuoi server",
"zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)"
"@@locale": "it",
"@@last_modified": "2021-06-24T23:32:06+02:00",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Non sincronizzato",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",
"shutdownCwtchTooltip": "Shutdown Cwtch",
"malformedMessage": "Malformed message",
"profileDeleteSuccess": "Successfully deleted profile",
"debugLog": "Turn on console debug logging",
"torNetworkStatus": "Tor network status",
"addContactFirst": "Add or pick a contact to begin chatting.",
"createProfileToBegin": "Please create or unlock a profile to begin",
"nickChangeSuccess": "Profile nickname changed successfully",
"addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"sendMessage": "Send Message",
"cancel": "Cancel",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "You sent an invitation for: ",
"contactSuggestion": "This is a contact suggestion for: ",
"rejected": "Rejected!",
"accepted": "Accepted!",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "... incolla qui un indirizzo per aggiungere un contatto...",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateMonthsAgo": "Months Ago",
"dateNever": "Never",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateLastYear": "Last Year",
"dateYesterday": "Yesterday",
"dateLastMonth": "Last Month",
"dateWeeksAgo": "Weeks Ago",
"dateDaysAgo": "Days Ago",
"dateHoursAgo": "Hours Ago",
"dateMinutesAgo": "Minutes Ago",
"dateRightNow": "Right Now",
"successfullAddedContact": "Successfully added ",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Esperimenti abilitati",
"localeIt": "Italiano",
"localeEs": "Spagnolo",
"addListItem": "Aggiungi un nuovo elemento alla lista",
"addNewItem": "Aggiungi un nuovo elemento alla lista",
"todoPlaceholder": "Da fare...",
"newConnectionPaneTitle": "Nuova connessione",
"networkStatusOnline": "Online",
"networkStatusConnecting": "Connessione alla rete e ai peer ...",
"networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor",
"networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione",
"viewGroupMembershipTooltip": "Visualizza i membri del gruppo",
"loadingTor": "Caricamento di tor...",
"smallTextLabel": "Piccolo",
"defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:",
"builddate": "Costruito il: %2",
"version": "Versione %1",
"versionTor": "Versione %1 con tor %2",
"themeDark": "Scuro",
"themeLight": "Chiaro",
"settingTheme": "Tema",
"largeTextLabel": "Grande",
"settingInterfaceZoom": "Livello di zoom",
"localeDe": "Tedesco",
"localePt": "Portoghese",
"localeFr": "Francese",
"localeEn": "Inglese",
"settingLanguage": "Lingua",
"blockUnknownLabel": "Blocca peer sconosciuti",
"zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)",
"versionBuilddate": "Versione: %1 Costruito il: %2",
"cwtchSettingsTitle": "Impostazioni di Cwtch",
"unlock": "Sblocca",
"yourServers": "I tuoi server",
"yourProfiles": "I tuoi profili",
"error0ProfilesLoadedForPassword": "0 profili caricati con quella password",
"password": "Password",
"enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili",
"addNewProfileBtn": "Aggiungi nuovo profilo",
"deleteConfirmText": "ELIMINA",
"deleteProfileConfirmBtn": "Elimina realmente il profilo",
"deleteConfirmLabel": "Digita ELIMINA per confermare",
"deleteProfileBtn": "Elimina profilo",
"passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata",
"passwordErrorMatch": "Le password non corrispondono",
"saveProfileBtn": "Salva il profilo",
"createProfileBtn": "Crea un profilo",
"passwordErrorEmpty": "La password non può essere vuota",
"password2Label": "Reinserire la password",
"password1Label": "Password",
"currentPasswordLabel": "Password corrente",
"yourDisplayName": "Il tuo nome visualizzato",
"profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
"noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati",
"radioNoPassword": "Non criptato (senza password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copiato negli appunti",
"copyBtn": "Copia",
"editProfile": "Modifica profilo",
"newProfile": "Nuovo profilo",
"defaultProfileName": "Alice",
"profileName": "Nome visualizzato",
"editProfileTitle": "Modifica profilo",
"addProfileTitle": "Aggiungi nuovo profilo",
"deleteBtn": "Elimina",
"unblockBtn": "Sblocca il peer",
"dontSavePeerHistory": "Elimina cronologia dei peer",
"savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
"savePeerHistory": "Salva cronologia peer",
"blockBtn": "Blocca il peer",
"saveBtn": "Salva",
"displayNameLabel": "Nome visualizzato",
"addressLabel": "Indirizzo",
"puzzleGameBtn": "Gioco di puzzle",
"bulletinsBtn": "Bollettini",
"listsBtn": "Liste",
"chatBtn": "Chat",
"rejectGroupBtn": "Rifiuta",
"acceptGroupBtn": "Accetta",
"acceptGroupInviteLabel": "Vuoi accettare l'invito a",
"newGroupBtn": "Crea un nuovo gruppo",
"copiedClipboardNotification": "Copiato negli Appunti",
"peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
"peerBlockedMessage": "Il peer è bloccato",
"pendingLabel": "In corso",
"acknowledgedLabel": "Riconosciuto",
"couldNotSendMsgError": "Impossibile inviare questo messaggio",
"dmTooltip": "Clicca per inviare un Messagio Diretto",
"membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.",
"addListItemBtn": "Aggiungi elemento",
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
"searchList": "Cerca nella lista",
"update": "Aggiornamento",
"inviteBtn": "Invitare",
"inviteToGroupLabel": "Invitare nel gruppo",
"groupNameLabel": "Nome del gruppo",
"viewServerInfo": "Informazioni sul server",
"serverSynced": "Sincronizzato",
"serverConnectivityDisconnected": "Server disconnesso",
"serverConnectivityConnected": "Server connesso",
"serverInfo": "Informazioni sul server",
"invitationLabel": "Invito",
"serverLabel": "Server",
"search": "Ricerca...",
"cycleColoursDesktop": "Fare clic per scorrere i colori.\nCliccare con il tasto destro per resettare.",
"cycleColoursAndroid": "Fare clic per scorrere i colori.\nPressione lunga per resettare.",
"cycleMorphsDesktop": "Fare clic per scorrere i morph.\nCliccare con il tasto destro per resettare.",
"cycleMorphsAndroid": "Fare clic per scorrere i morph.\nPressione lunga per resettare.",
"cycleCatsDesktop": "Fare clic per scorrere le categorie.\nCliccare con il tasto destro per resettare.",
"cycleCatsAndroid": "Fare clic per scorrere le categorie.\nPressione lunga per resettare.",
"blocked": "Bloccato",
"titlePlaceholder": "titolo...",
"postNewBulletinLabel": "Pubblica un nuovo bollettino",
"newBulletinLabel": "Nuovo bollettino",
"joinGroup": "Unisciti al gruppo",
"createGroup": "Crea un gruppo",
"addPeer": "Aggiungi peer",
"groupAddr": "Indirizzo",
"invitation": "Invito",
"server": "Server",
"groupName": "Nome del gruppo",
"peerName": "Nome",
"peerAddress": "Indirizzo",
"joinGroupTab": "Unisciti a un gruppo",
"createGroupTab": "Crea un gruppo",
"addPeerTab": "Aggiungi un peer",
"createGroupBtn": "Crea",
"defaultGroupName": "Gruppo fantastico",
"createGroupTitle": "Crea un gruppo"
}

View File

@ -1,160 +1,193 @@
{
"@@locale": "pt",
"acceptGroupBtn": "Aceitar",
"acceptGroupInviteLabel": "Você quer aceitar o convite para",
"acknowledgedLabel": "Confirmada",
"addListItem": "Adicionar Item à Lista",
"addListItemBtn": "",
"addNewItem": "Adicionar novo item à lista",
"addNewProfileBtn": "",
"addPeer": "",
"addPeerTab": "",
"addProfileTitle": "",
"addressLabel": "Endereço",
"blockBtn": "",
"blocked": "",
"blockUnknownLabel": "",
"builddate": "",
"bulletinsBtn": "Boletins",
"chatBtn": "Chat",
"contactAlreadyExists": "",
"conversationSettings": "",
"copiedClipboardNotification": "Copiado",
"copiedToClipboardNotification": "Copiado",
"copyBtn": "Copiar",
"couldNotSendMsgError": "Não deu para enviar esta mensagem",
"createGroup": "",
"createGroupBtn": "Criar",
"createGroupTab": "",
"createGroupTitle": "Criar Grupo",
"createProfileBtn": "",
"currentPasswordLabel": "",
"cwtchSettingsTitle": "Configurações do Cwtch",
"cycleCatsAndroid": "",
"cycleCatsDesktop": "",
"cycleColoursAndroid": "",
"cycleColoursDesktop": "",
"cycleMorphsAndroid": "",
"cycleMorphsDesktop": "",
"dateDaysAgo": "",
"dateHoursAgo": "",
"dateLastMonth": "",
"dateLastYear": "",
"dateMinutesAgo": "",
"dateMonthsAgo": "",
"dateNever": "",
"dateRightNow": "",
"dateWeeksAgo": "",
"dateYearsAgo": "",
"dateYesterday": "",
"defaultGroupName": "Grupo incrível",
"defaultProfileName": "",
"defaultScalingText": "Texto tamanho padrão (fator de escala: ",
"deleteBtn": "Deletar",
"deleteConfirmLabel": "",
"deleteConfirmText": "",
"deleteProfileBtn": "",
"deleteProfileConfirmBtn": "",
"descriptionBlockUnknownConnections": "",
"descriptionExperiments": "",
"descriptionExperimentsGroups": "",
"displayNameLabel": "Nome de Exibição",
"dmTooltip": "Clique para DM",
"dontSavePeerHistory": "",
"editProfile": "",
"editProfileTitle": "",
"enableGroups": "",
"enterCurrentPasswordForDelete": "",
"enterProfilePassword": "",
"error0ProfilesLoadedForPassword": "",
"experimentsEnabled": "",
"groupAddr": "",
"groupName": "",
"groupNameLabel": "Nome do Grupo",
"invalidImportString": "",
"invitation": "",
"invitationLabel": "Convite",
"inviteBtn": "Convidar",
"inviteToGroupLabel": "Convidar ao grupo",
"joinGroup": "",
"joinGroupTab": "",
"largeTextLabel": "Grande",
"listsBtn": "Listas",
"loadingTor": "",
"localeDe": "",
"localeEn": "",
"localeEs": "",
"localeFr": "",
"localeIt": "",
"localePt": "",
"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.",
"networkStatusAttemptingTor": "",
"networkStatusConnecting": "",
"networkStatusDisconnected": "",
"networkStatusOnline": "",
"newBulletinLabel": "Novo Boletim",
"newConnectionPaneTitle": "",
"newGroupBtn": "Criar novo grupo",
"newProfile": "",
"noPasswordWarning": "",
"password": "",
"password1Label": "",
"password2Label": "",
"passwordChangeError": "",
"passwordErrorEmpty": "",
"passwordErrorMatch": "",
"pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…",
"peerAddress": "",
"peerBlockedMessage": "",
"peerName": "",
"peerNotOnline": "",
"peerOfflineMessage": "",
"pendingLabel": "Pendente",
"postNewBulletinLabel": "Postar novo boletim",
"profileName": "",
"profileOnionLabel": "",
"puzzleGameBtn": "Jogo de Adivinhação",
"radioNoPassword": "",
"radioUsePassword": "",
"rejectGroupBtn": "Recusar",
"saveBtn": "Salvar",
"savePeerHistory": "",
"savePeerHistoryDescription": "",
"saveProfileBtn": "",
"search": "",
"searchList": "",
"server": "",
"serverConnectivityConnected": "",
"serverConnectivityDisconnected": "",
"serverInfo": "",
"serverLabel": "Servidor",
"serverNotSynced": "",
"serverSynced": "",
"settingInterfaceZoom": "",
"settingLanguage": "",
"settingTheme": "",
"smallTextLabel": "Pequeno",
"successfullAddedContact": "",
"themeDark": "",
"themeLight": "",
"titleManageContacts": "",
"titleManageProfiles": "",
"titleManageServers": "",
"titlePlaceholder": "título…",
"todoPlaceholder": "Afazer…",
"tooltipAddContact": "",
"tooltipOpenSettings": "",
"tooltipUnlockProfiles": "",
"unblockBtn": "",
"unlock": "",
"update": "",
"version": "",
"versionBuilddate": "",
"versionTor": "",
"viewGroupMembershipTooltip": "",
"viewServerInfo": "",
"yourDisplayName": "",
"yourProfiles": "",
"yourServers": "",
"zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)"
"@@locale": "pt",
"@@last_modified": "2021-06-24T23:32:06+02:00",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",
"shutdownCwtchTooltip": "Shutdown Cwtch",
"malformedMessage": "Malformed message",
"profileDeleteSuccess": "Successfully deleted profile",
"debugLog": "Turn on console debug logging",
"torNetworkStatus": "Tor network status",
"addContactFirst": "Add or pick a contact to begin chatting.",
"createProfileToBegin": "Please create or unlock a profile to begin",
"nickChangeSuccess": "Profile nickname changed successfully",
"addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"sendMessage": "Send Message",
"cancel": "Cancel",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "You sent an invitation for: ",
"contactSuggestion": "This is a contact suggestion for: ",
"rejected": "Rejected!",
"accepted": "Accepted!",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateMonthsAgo": "Months Ago",
"dateNever": "Never",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateLastYear": "Last Year",
"dateYesterday": "Yesterday",
"dateLastMonth": "Last Month",
"dateWeeksAgo": "Weeks Ago",
"dateDaysAgo": "Days Ago",
"dateHoursAgo": "Hours Ago",
"dateMinutesAgo": "Minutes Ago",
"dateRightNow": "Right Now",
"successfullAddedContact": "Successfully added ",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists",
"conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat",
"experimentsEnabled": "Enable Experiments",
"localeIt": "Italiana",
"localeEs": "Espanol",
"addListItem": "Adicionar Item à Lista",
"addNewItem": "Adicionar novo item à lista",
"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",
"loadingTor": "Loading tor...",
"smallTextLabel": "Pequeno",
"defaultScalingText": "Texto tamanho padrão (fator de escala: ",
"builddate": "Built on: %2",
"version": "Version %1",
"versionTor": "Version %1 with tor %2",
"themeDark": "Dark",
"themeLight": "Light",
"settingTheme": "Theme",
"largeTextLabel": "Grande",
"settingInterfaceZoom": "Zoom level",
"localeDe": "Deutsche",
"localePt": "Portuguesa",
"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",
"unlock": "Unlock",
"yourServers": "Your Servers",
"yourProfiles": "Your Profiles",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
"password": "Password",
"enterProfilePassword": "Enter a password to view your profiles",
"addNewProfileBtn": "Add new profile",
"deleteConfirmText": "DELETE",
"deleteProfileConfirmBtn": "Really Delete Profile",
"deleteConfirmLabel": "Type DELETE to confirm",
"deleteProfileBtn": "Delete Profile",
"passwordChangeError": "Error changing password: Supplied password rejected",
"passwordErrorMatch": "Passwords do not match",
"saveProfileBtn": "Save Profile",
"createProfileBtn": "Create Profile",
"passwordErrorEmpty": "Password cannot be empty",
"password2Label": "Reenter password",
"password1Label": "Password",
"currentPasswordLabel": "Current Password",
"yourDisplayName": "Your Display Name",
"profileOnionLabel": "Send this address to peers you want to connect with",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copiado",
"copyBtn": "Copiar",
"editProfile": "Edit Profille",
"newProfile": "New Profile",
"defaultProfileName": "Alice",
"profileName": "Display name",
"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",
"puzzleGameBtn": "Jogo de Adivinhação",
"bulletinsBtn": "Boletins",
"listsBtn": "Listas",
"chatBtn": "Chat",
"rejectGroupBtn": "Recusar",
"acceptGroupBtn": "Aceitar",
"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",
"inviteToGroupLabel": "Convidar ao grupo",
"groupNameLabel": "Nome do Grupo",
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected",
"serverInfo": "Server Information",
"invitationLabel": "Convite",
"serverLabel": "Servidor",
"search": "Search...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked",
"titlePlaceholder": "título…",
"postNewBulletinLabel": "Postar novo boletim",
"newBulletinLabel": "Novo Boletim",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
"groupName": "Group name",
"peerName": "Name",
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Criar",
"defaultGroupName": "Grupo incrível",
"createGroupTitle": "Criar Grupo"
}

View File

@ -114,4 +114,7 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''');
yield LicenseEntryWithLineBreaks(["flaticons"], "Icons made by Freepik (https://www.freepik.com) from Flaticon (www.flaticon.com)");
}

View File

@ -1,4 +1,6 @@
import 'dart:convert';
import 'package:cwtch/notification_manager.dart';
import 'package:cwtch/views/messageview.dart';
import 'package:cwtch/widgets/rightshiftfixer.dart';
import 'package:flutter/foundation.dart';
import 'package:cwtch/cwtch/ffi.dart';
@ -8,6 +10,7 @@ import 'package:cwtch/errorHandler.dart';
import 'package:cwtch/settings.dart';
import 'package:cwtch/torstatus.dart';
import 'package:cwtch/views/triplecolview.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'cwtch/cwtch.dart';
import 'cwtch/cwtchNotifier.dart';
@ -15,16 +18,20 @@ import 'licenses.dart';
import 'model.dart';
import 'views/profilemgrview.dart';
import 'views/splashView.dart';
import 'dart:io' show Platform;
import 'dart:io' show Platform, exit;
import 'opaque.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var globalSettings = Settings(Locale("en", ''), Opaque.dark);
var globalSettings = Settings(Locale("en", ''), OpaqueDark());
var globalErrorHandler = ErrorHandler();
var globalTorStatus = TorStatus();
var globalAppState = AppState();
void main() {
print("main()");
LicenseRegistry.addLicense(() => licenses());
WidgetsFlutterBinding.ensureInitialized();
print("runApp()");
runApp(Flwtch());
}
@ -38,47 +45,45 @@ class Flwtch extends StatefulWidget {
class FlwtchState extends State<Flwtch> {
final TextStyle biggerFont = const TextStyle(fontSize: 18);
late Cwtch cwtch;
bool cwtchInit = false;
late ProfileInfoState selectedProfile;
String selectedConversation = "";
var columns = [1]; // default or 'single column' mode
//var columns = [1, 1, 2];
late ProfileListState profs;
final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler');
final MethodChannel shutdownMethodChannel = MethodChannel('im.cwtch.flwtch/shutdown');
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
@override
initState() {
print("initState: running...");
super.initState();
cwtchInit = false;
print("initState: registering notification, shutdown handlers...");
profs = ProfileListState();
notificationClickChannel.setMethodCallHandler(_externalNotificationClicked);
shutdownMethodChannel.setMethodCallHandler(shutdown);
print("initState: creating cwtchnotifier, ffi");
if (Platform.isAndroid) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager());
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
cwtch = CwtchGomobile(cwtchNotifier);
} else if (Platform.isLinux) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, LinuxNotificationsManager());
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, LinuxNotificationsManager(), globalAppState);
cwtch = CwtchFfi(cwtchNotifier);
} else {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager());
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
cwtch = CwtchFfi(cwtchNotifier);
}
cwtch.Start().then((val) {
setState(() {
cwtchInit = true;
});
});
print("initState: invoking cwtch.Start()");
cwtch.Start();
print("initState: done!");
}
ChangeNotifierProvider<TorStatus> getTorStatusProvider() => ChangeNotifierProvider.value(value: globalTorStatus);
ChangeNotifierProvider<ErrorHandler> getErrorHandlerProvider() => ChangeNotifierProvider.value(value: globalErrorHandler);
ChangeNotifierProvider<Settings> getSettingsProvider() => ChangeNotifierProvider.value(value: globalSettings);
ChangeNotifierProvider<AppState> getAppStateProvider() => ChangeNotifierProvider.value(value: globalAppState);
Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs);
@override
Widget build(BuildContext context) {
//appStatus = AppModel(cwtch: cwtch);
globalSettings.initPackageInfo();
return MultiProvider(
providers: [
@ -87,25 +92,72 @@ class FlwtchState extends State<Flwtch> {
getSettingsProvider(),
getErrorHandlerProvider(),
getTorStatusProvider(),
getAppStateProvider(),
],
builder: (context, widget) {
return Consumer<Settings>(
builder: (context, settings, child) => MaterialApp(
return Consumer2<Settings, AppState>(
builder: (context, settings, appState, child) => MaterialApp(
key: Key('app'),
navigatorKey: navKey,
locale: settings.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'Cwtch',
theme: mkThemeData(settings),
// from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(),
// from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(),
home: cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ShiftRightFixer(child: ProfileMgrView())) : SplashView(),
home: appState.cwtchInit == true ? ShiftRightFixer(child: ProfileMgrView()) : SplashView(),
),
);
},
);
}
Future<void> shutdown(MethodCall call) async {
cwtch.Shutdown();
// Wait a few seconds as shutting down things takes a little time..
Future.delayed(Duration(seconds: 2)).then((value) {
if (Platform.isAndroid) {
SystemNavigator.pop();
} else if (Platform.isLinux || Platform.isWindows) {
print("Exiting...");
exit(0);
}
});
}
// Invoked via notificationClickChannel by MyBroadcastReceiver in MainActivity.kt
// coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID
Future<void> _externalNotificationClicked(MethodCall call) async {
var args = jsonDecode(call.arguments);
var profile = profs.getProfile(args["ProfileOnion"])!;
var contact = profile.contactList.getContact(args["RemotePeer"])!;
contact.unreadMessages = 0;
// single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation
var isLandscape = Provider.of<AppState>(navKey.currentContext!, listen: false).isLandscape(navKey.currentContext!);
if (Provider.of<Settings>(navKey.currentContext!, listen: false).uiColumns(isLandscape).length == 1) {
if (navKey.currentContext?.findAncestorWidgetOfExactType<MessageView>() != null) {
print("messageview already open; popping before pushing replacement");
navKey.currentState?.pop();
}
navKey.currentState?.push(
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: contact),
],
builder: (context, child) => MessageView(),
);
},
),
);
} else { //dual pane
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedProfile = args["ProfileOnion"];
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = args["RemotePeer"];
}
}
@override
void dispose() {
cwtch.dispose();

View File

@ -13,9 +13,8 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
var globalSettings = Settings(Locale("en", ''), Opaque.dark);
var globalSettings = Settings(Locale("en", ''), OpaqueDark());
var globalErrorHandler = ErrorHandler();
void main() {

View File

@ -38,13 +38,13 @@ class ProfileListState extends ChangeNotifier {
List<ProfileInfoState> _profiles = [];
int get num => _profiles.length;
void addAll(Iterable<ProfileInfoState> newProfiles) {
_profiles.addAll(newProfiles);
notifyListeners();
}
void add(ProfileInfoState newProfile) {
_profiles.add(newProfile);
void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online, bool encrypted) {
var idx = _profiles.indexWhere((element) => element.onion == onion);
if (idx == -1) {
_profiles.add(ProfileInfoState(onion: onion, nickname: name, imagePath: picture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted));
} else {
_profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online);
}
notifyListeners();
}
@ -54,6 +54,42 @@ class ProfileListState extends ChangeNotifier {
int idx = _profiles.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _profiles[idx] : null;
}
void delete(String onion) {
_profiles.removeWhere((element) => element.onion == onion);
notifyListeners();
}
}
class AppState extends ChangeNotifier {
bool cwtchInit = false;
String appError = "";
String? _selectedProfile;
String? _selectedConversation;
void SetCwtchInit() {
cwtchInit = true;
notifyListeners();
}
void SetAppError(String error) {
appError = error;
notifyListeners();
}
String? get selectedProfile => _selectedProfile;
set selectedProfile(String? newVal) {
this._selectedProfile = newVal;
notifyListeners();
}
String? get selectedConversation => _selectedConversation;
set selectedConversation(String? newVal) {
this._selectedConversation = newVal;
notifyListeners();
}
bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height;
}
class ContactListState extends ChangeNotifier {
@ -64,13 +100,13 @@ class ContactListState extends ChangeNotifier {
bool get isFiltered => _filter != "";
String get filter => _filter;
set filter(String newVal) {
_filter = newVal;
_filter = newVal.toLowerCase();
notifyListeners();
}
List<ContactInfoState> filteredList() {
if (!isFiltered) return contacts;
return _contacts.where((ContactInfoState c) => c.onion.contains(_filter) || (c.nickname.contains(_filter))).toList();
return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList();
}
void addAll(Iterable<ContactInfoState> newContacts) {
@ -141,6 +177,10 @@ class ProfileInfoState extends ChangeNotifier {
int _unreadMessages = 0;
bool _online = false;
// assume profiles are encrypted...this will be set to false
// in the constructor if the profile is encrypted with the defacto password.
bool _encrypted = true;
ProfileInfoState({
required this.onion,
nickname = "",
@ -149,11 +189,13 @@ class ProfileInfoState extends ChangeNotifier {
contactsJson = "",
serversJson = "",
online = false,
encrypted = true,
}) {
this._nickname = nickname;
this._imagePath = imagePath;
this._unreadMessages = unreadMessages;
this._online = online;
this._encrypted = encrypted;
if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
List<dynamic> contacts = jsonDecode(contactsJson);
@ -183,7 +225,7 @@ class ProfileInfoState extends ChangeNotifier {
// Parse out the server list json into our server info state struct...
void replaceServers(String serversJson) {
if (serversJson != null && serversJson != "" && serversJson != "null") {
if (serversJson != "" && serversJson != "null") {
print("got servers $serversJson");
List<dynamic> servers = jsonDecode(serversJson);
this._servers.replace(servers.map((server) {
@ -207,6 +249,9 @@ class ProfileInfoState extends ChangeNotifier {
notifyListeners();
}
// Check encrypted status for profile info screen
bool get isEncrypted => this._encrypted;
String get nickname => this._nickname;
set nickname(String newValue) {
this._nickname = newValue;
@ -241,6 +286,41 @@ class ProfileInfoState extends ChangeNotifier {
super.dispose();
print("profileinfostate.dispose()");
}
void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) {
this._nickname = name;
this._imagePath = picture;
this._online = online;
this.replaceServers(serverJson);
if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
List<dynamic> contacts = jsonDecode(contactsJson);
contacts.forEach((contact) {
var profileContact = this._contacts.getContact(contact["onion"]);
if (profileContact != null) {
profileContact.status = contact["status"];
profileContact.totalMessages = contact["numMessages"];
profileContact.lastMessageTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"]));
} else {
this._contacts.add(ContactInfoState(
this.onion,
contact["onion"],
nickname: contact["name"],
status: contact["status"],
imagePath: contact["picture"],
isBlocked: contact["authorization"] == "blocked",
isInvitation: contact["authorization"] == "unknown",
savePeerHistory: contact["saveConversationHistory"],
numMessages: contact["numMessages"],
numUnread: contact["numUnread"],
isGroup: contact["isGroup"],
server: contact["groupServer"],
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])),
));
}
});
}
}
}
class ContactInfoState extends ChangeNotifier {
@ -357,7 +437,8 @@ class ContactInfoState extends ChangeNotifier {
bool isOnline() {
if (this.isGroup == true) {
return this.status == "Synced";
// We now have an out of sync warning so we will mark these as online...
return this.status == "Authenticated" || this.status == "Synced";
} else {
return this.status == "Authenticated";
}
@ -382,7 +463,8 @@ class MessageState extends ChangeNotifier {
late String _inviteNick;
late DateTime _timestamp;
late String _senderOnion;
late String _senderImage;
late int _flags;
String? _senderImage;
late String _signature = "";
late bool _ackd = false;
late bool _error = false;
@ -402,12 +484,18 @@ class MessageState extends ChangeNotifier {
get message => this._message;
get overlay => this._overlay;
get timestamp => this._timestamp;
int get flags => this._flags;
set flags(int newVal) {
this._flags = newVal;
notifyListeners();
}
bool get ackd => this._ackd;
bool get error => this._error;
get malformed => this._malformed;
bool get malformed => this._malformed;
bool get loaded => this._loaded;
get senderOnion => this._senderOnion;
get senderImage => this._senderImage;
get loaded => this._loaded;
get signature => this._signature;
get isInvite => this.overlay == 100 || this.overlay == 101;
get inviteTarget => this._inviteTarget;
@ -423,13 +511,24 @@ class MessageState extends ChangeNotifier {
notifyListeners();
}
set malformed(bool newVal) {
this._malformed = newVal;
notifyListeners();
}
set loaded(bool newVal) {
// quickly-arriving messages get discarded before loading sometimes
if (!hasListeners) return;
this._loaded = newVal;
notifyListeners();
}
void tryLoad(BuildContext context) {
Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) {
try {
dynamic messageWrapper = jsonDecode(jsonMessage);
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
this._senderOnion = profileOnion;
//todo: remove once sent group messages are prestored
Future.delayed(const Duration(milliseconds: 2), () {
tryLoad(context);
});
@ -441,6 +540,7 @@ class MessageState extends ChangeNotifier {
this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
this._senderOnion = messageWrapper['PeerID'];
this._senderImage = messageWrapper['ContactImage'];
this._flags = int.parse(messageWrapper['Flags'].toString(), radix: 2);
// If this is a group, store the signature
if (contactHandle.length == 32) {
@ -456,7 +556,6 @@ class MessageState extends ChangeNotifier {
} else {
var parts = message['d'].toString().split("||");
if (parts.length == 2) {
print("jsondecoding: " + utf8.fuse(base64).decode(parts[1].substring(5)));
var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5)));
this._inviteTarget = jsonObj['GroupID'];
this._inviteNick = jsonObj['GroupName'];
@ -464,7 +563,7 @@ class MessageState extends ChangeNotifier {
}
}
this._loaded = true;
this.loaded = true;
//update ackd and error last as they are changenotified
this.ackd = messageWrapper['Acknowledged'];
@ -472,7 +571,9 @@ class MessageState extends ChangeNotifier {
this.error = true;
}
} catch (e) {
this._malformed = true;
this._overlay = -1;
this.loaded = true;
this.malformed = true;
}
});
}

View File

@ -16,11 +16,10 @@ class NullNotificationsManager implements NotificationsManager {
// the standard dbus-powered linux desktop notifications.
class LinuxNotificationsManager implements NotificationsManager {
int previous_id = 0;
final NotificationsClient client = NotificationsClient();
LinuxNotificationsManager() {}
Future<void> notify(String message) async {
var client = NotificationsClient();
var icon_path = Uri.file(path.join(path.current, "cwtch.png"));
client.notify('New Message from Peer!', appName: "cwtch", appIcon: icon_path.toString(), replacesId: this.previous_id).then((Notification value) => previous_id = value.id);
client.close();
}
}

View File

@ -10,6 +10,11 @@ import 'package:cwtch/settings.dart';
abstract class OpaqueThemeType {
static final Color red = Color(0xFFFF0000);
String identifier() {
return "dummy";
}
Color backgroundMainColor() {
return red;
}
@ -304,9 +309,15 @@ abstract class OpaqueThemeType {
// ... more to come
// Sizes
double contactOnionTextSize() {
return 18;
}
}
class CwtchDark extends OpaqueThemeType {
class OpaqueDark extends OpaqueThemeType {
static final Color darkGreyPurple = Color(0xFF281831);
static final Color deepPurple = Color(0xFF422850);
static final Color mauvePurple = Color(0xFF8E64A5);
@ -319,6 +330,10 @@ class CwtchDark extends OpaqueThemeType {
static final Color softGreen = Color(0xFFA0FFB0);
static final Color softRed = Color(0xFFFFA0B0);
String identifier() {
return "dark";
}
Color backgroundMainColor() {
return darkGreyPurple;
}
@ -612,7 +627,7 @@ class CwtchDark extends OpaqueThemeType {
}
}
class CwtchLight extends OpaqueThemeType {
class OpaqueLight extends OpaqueThemeType {
static final Color whitePurple = Color(0xFFFFFDFF);
static final Color softPurple = Color(0xFFFDF3FC);
static final Color purple = Color(0xFFDFB9DE);
@ -625,6 +640,10 @@ class CwtchLight extends OpaqueThemeType {
static final Color softGreen = Color(0xFFA0FFB0);
static final Color softRed = Color(0xFFFFA0B0);
String identifier() {
return "light";
}
Color backgroundMainColor() {
return whitePurple;
}
@ -918,6 +937,99 @@ class CwtchLight extends OpaqueThemeType {
}
}
ThemeData mkThemeData(Settings opaque) {
return ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
primarySwatch: Colors.red,
primaryIconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
primaryColor: opaque.current().backgroundMainColor(),
canvasColor: opaque.current().backgroundPaneColor(),
backgroundColor: opaque.current().backgroundMainColor(),
highlightColor: opaque.current().hilightElementTextColor(),
iconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
cardColor: opaque.current().backgroundMainColor(),
appBarTheme: AppBarTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(
color: opaque.current().mainTextColor(),
),
actionsIconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
)),
bottomNavigationBarTheme: BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed, backgroundColor: opaque.current().backgroundHilightElementColor()),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
)),
),
),
scrollbarTheme: ScrollbarThemeData(
isAlwaysShown: false, thumbColor: MaterialStateProperty.all(opaque.current().scrollbarActiveColor()), trackColor: MaterialStateProperty.all(opaque.current().scrollbarDefaultColor())),
tabBarTheme: TabBarTheme(indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor()))),
dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(color: opaque.current().mainTextColor()),
contentTextStyle: TextStyle(color: opaque.current().mainTextColor())),
textTheme: TextTheme(
headline1: TextStyle(color: opaque.current().mainTextColor()),
headline2: TextStyle(color: opaque.current().mainTextColor()),
headline3: TextStyle(color: opaque.current().mainTextColor()),
headline4: TextStyle(color: opaque.current().mainTextColor()),
headline5: TextStyle(color: opaque.current().mainTextColor()),
headline6: TextStyle(color: opaque.current().mainTextColor()),
bodyText1: TextStyle(color: opaque.current().mainTextColor()),
bodyText2: TextStyle(color: opaque.current().mainTextColor()),
subtitle1: TextStyle(color: opaque.current().mainTextColor()),
subtitle2: TextStyle(color: opaque.current().mainTextColor()),
caption: TextStyle(color: opaque.current().mainTextColor()),
button: TextStyle(color: opaque.current().mainTextColor()),
overline: TextStyle(color: opaque.current().mainTextColor())),
switchTheme: SwitchThemeData(
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
thumbColor: MaterialStateProperty.all(opaque.current().mainTextColor()),
trackColor: MaterialStateProperty.all(opaque.current().dropShadowColor()),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(backgroundColor: opaque.current().defaultButtonColor(), hoverColor: opaque.current().defaultButtonActiveColor()),
textSelectionTheme: TextSelectionThemeData(
cursorColor: opaque.current().defaultButtonActiveColor(), selectionColor: opaque.current().defaultButtonActiveColor(), selectionHandleColor: opaque.current().defaultButtonActiveColor()),
);
}
/*
OpaqueThemeType _current = CwtchDark();
void setDark() {
_current = CwtchDark();
}
void setLight() {
_current = CwtchLight();
}
OpaqueThemeType current() {
if (_current == null) {
setDark();
}
return _current;
}
class Opaque extends OpaqueThemeType {
Color backgroundMainColor() {
return current().backgroundMainColor();
@ -1226,22 +1338,9 @@ class Opaque extends OpaqueThemeType {
}
static late OpaqueThemeType _current;
static final OpaqueThemeType dark = CwtchDark();
static final OpaqueThemeType light = CwtchLight();
static void setDark() {
_current = dark;
}
//static final OpaqueThemeType dark = CwtchDark();
//static final OpaqueThemeType light = CwtchLight();
static void setLight() {
_current = light;
}
static OpaqueThemeType current() {
if (_current == null) {
setDark();
}
return _current;
}
int scale = 2;
static final String gcdOS = "linux";
@ -1341,74 +1440,4 @@ class Opaque extends OpaqueThemeType {
}
}
ThemeData mkThemeData(Settings opaque) {
return ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
primarySwatch: Colors.red,
primaryIconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
primaryColor: opaque.current().backgroundMainColor(),
canvasColor: opaque.current().backgroundPaneColor(),
backgroundColor: opaque.current().backgroundMainColor(),
highlightColor: opaque.current().hilightElementTextColor(),
iconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
cardColor: opaque.current().backgroundMainColor(),
appBarTheme: AppBarTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(
color: opaque.current().mainTextColor(),
),
actionsIconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
)),
),
),
tabBarTheme: TabBarTheme(indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor()))),
dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(color: opaque.current().mainTextColor()),
contentTextStyle: TextStyle(color: opaque.current().mainTextColor())),
textTheme: TextTheme(
headline1: TextStyle(color: opaque.current().mainTextColor()),
headline2: TextStyle(color: opaque.current().mainTextColor()),
headline3: TextStyle(color: opaque.current().mainTextColor()),
headline4: TextStyle(color: opaque.current().mainTextColor()),
headline5: TextStyle(color: opaque.current().mainTextColor()),
headline6: TextStyle(color: opaque.current().mainTextColor()),
bodyText1: TextStyle(color: opaque.current().mainTextColor()),
bodyText2: TextStyle(color: opaque.current().mainTextColor()),
subtitle1: TextStyle(color: opaque.current().mainTextColor()),
subtitle2: TextStyle(color: opaque.current().mainTextColor()),
caption: TextStyle(color: opaque.current().mainTextColor()),
button: TextStyle(color: opaque.current().mainTextColor()),
overline: TextStyle(color: opaque.current().mainTextColor())),
switchTheme: SwitchThemeData(
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
thumbColor: MaterialStateProperty.all(opaque.current().mainTextColor()),
trackColor: MaterialStateProperty.all(opaque.current().dropShadowColor()),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(backgroundColor: opaque.current().defaultButtonColor(), hoverColor: opaque.current().defaultButtonActiveColor()),
textSelectionTheme: TextSelectionThemeData(
cursorColor: opaque.current().defaultButtonActiveColor(), selectionColor: opaque.current().defaultButtonActiveColor(), selectionHandleColor: opaque.current().defaultButtonActiveColor()),
);
}
*/

View File

@ -9,6 +9,13 @@ import 'opaque.dart';
const TapirGroupsExperiment = "tapir-groups-experiment";
enum DualpaneMode {
Single,
Dual1to2,
Dual1to4,
CopyPortrait,
}
/// Settings govern the *Globally* relevant settings like Locale, Theme and Experiments.
/// We also provide access to the version information here as it is also accessed from the
/// Settings Pane.
@ -16,20 +23,23 @@ class Settings extends ChangeNotifier {
Locale locale;
late PackageInfo packageInfo;
OpaqueThemeType theme;
late bool experimentsEnabled;
// explicitly set experiments to false until told otherwise...
bool experimentsEnabled = false;
HashMap<String, bool> experiments = HashMap.identity();
DualpaneMode _uiColumnModePortrait = DualpaneMode.Single;
DualpaneMode _uiColumnModeLandscape = DualpaneMode.CopyPortrait;
late bool blockUnknownConnections;
bool blockUnknownConnections = false;
/// Set the dark theme.
void setDark() {
theme = Opaque.dark;
theme = OpaqueDark();
notifyListeners();
}
/// Set the Light theme.
void setLight() {
theme = Opaque.light;
theme = OpaqueLight();
notifyListeners();
}
@ -38,6 +48,18 @@ class Settings extends ChangeNotifier {
return theme;
}
/// isExperimentEnabled can be used to safely check whether a particular
/// experiment is enabled
bool isExperimentEnabled(String experiment) {
if (this.experimentsEnabled) {
if (this.experiments.containsKey(experiment)) {
// We now know it cannot be null...
return this.experiments[experiment]! == true;
}
}
return false;
}
/// Called by the event bus. When new settings are loaded from a file the JSON will
/// be sent to the function and new settings will be instantiated based on the contents.
handleUpdate(dynamic settings) {
@ -60,6 +82,10 @@ class Settings extends ChangeNotifier {
// Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON
experiments = new HashMap<String, bool>.from(settings["Experiments"]);
// single pane vs dual pane preferences
_uiColumnModePortrait = uiColumnModeFromString(settings["UIColumnModePortrait"]);
_uiColumnModeLandscape = uiColumnModeFromString(settings["UIColumnModeLandscape"]);
// Push the experimental settings to Consumers of Settings
notifyListeners();
}
@ -121,16 +147,62 @@ class Settings extends ChangeNotifier {
notifyListeners();
}
DualpaneMode get uiColumnModePortrait => _uiColumnModePortrait;
set uiColumnModePortrait(DualpaneMode newval) {
this._uiColumnModePortrait = newval;
notifyListeners();
}
DualpaneMode get uiColumnModeLandscape => _uiColumnModeLandscape;
set uiColumnModeLandscape(DualpaneMode newval) {
this._uiColumnModeLandscape = newval;
notifyListeners();
}
List<int> uiColumns(bool isLandscape) {
var m = (!isLandscape || uiColumnModeLandscape == DualpaneMode.CopyPortrait) ? uiColumnModePortrait : uiColumnModeLandscape;
switch(m) {
case DualpaneMode.Single: return [1];
case DualpaneMode.Dual1to2: return [1, 2];
case DualpaneMode.Dual1to4: return [1, 4];
}
print("impossible column configuration: portrait/$uiColumnModePortrait landscape/$uiColumnModeLandscape");
return [1];
}
static List<DualpaneMode> uiColumnModeOptions(bool isLandscape) {
if (isLandscape) return [DualpaneMode.CopyPortrait, DualpaneMode.Single, DualpaneMode.Dual1to2, DualpaneMode.Dual1to4,];
else return [DualpaneMode.Single, DualpaneMode.Dual1to2, DualpaneMode.Dual1to4];
}
static DualpaneMode uiColumnModeFromString(String m) {
switch(m) {
case "DualpaneMode.Single": return DualpaneMode.Single;
case "DualpaneMode.Dual1to2": return DualpaneMode.Dual1to2;
case "DualpaneMode.Dual1to4": return DualpaneMode.Dual1to4;
case "DualpaneMode.CopyPortrait": return DualpaneMode.CopyPortrait;
}
print("Error: ui requested translation of column mode [$m] which doesn't exist");
return DualpaneMode.Single;
}
static String uiColumnModeToString(DualpaneMode m) {
// todo: translate
switch(m) {
case DualpaneMode.Single: return "Single";
case DualpaneMode.Dual1to2: return "Double (1:2)";
case DualpaneMode.Dual1to4: return "Double (1:4)";
case DualpaneMode.CopyPortrait: return "Same as portrait mode setting";
}
}
/// Construct a default settings object.
Settings(this.locale, this.theme);
/// Convert this Settings object to a JSON representation for serialization on the
/// event bus.
dynamic asJson() {
var themeString = "light";
if (theme == Opaque.dark) {
themeString = "dark";
}
var themeString = theme.identifier();
return {
"Locale": this.locale.languageCode,
@ -140,7 +212,9 @@ class Settings extends ChangeNotifier {
"ExperimentsEnabled": this.experimentsEnabled,
"Experiments": experiments,
"StateRootPane": 0,
"FirstTime": false
"FirstTime": false,
"UIColumnModePortrait": uiColumnModePortrait.toString(),
"UIColumnModeLandscape": uiColumnModeLandscape.toString(),
};
}
}

View File

@ -4,8 +4,9 @@ class TorStatus extends ChangeNotifier {
int progress;
String status;
bool connected;
String version;
TorStatus({this.connected = false, this.progress = 0, this.status = ""});
TorStatus({this.connected = false, this.progress = 0, this.status = "", this.version = ""});
/// Called by the event bus.
handleUpdate(int new_progress, String new_status) {
@ -20,4 +21,9 @@ class TorStatus extends ChangeNotifier {
notifyListeners();
}
updateVersion(String new_version) {
version = new_version;
notifyListeners();
}
}

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/errorHandler.dart';
@ -49,8 +50,8 @@ class _AddContactViewState extends State<AddContactView> {
Widget _buildForm() {
ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion;
/// We display a different number of tabs dependening on the experiment setup
bool groupsEnabled = Provider.of<Settings>(context).experimentsEnabled && Provider.of<Settings>(context).experiments[TapirGroupsExperiment]!;
/// We display a different number of tabs depending on the experiment setup
bool groupsEnabled = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
return Consumer<ErrorHandler>(builder: (context, globalErrorHandler, child) {
return DefaultTabController(
length: groupsEnabled ? 2 : 1,
@ -80,7 +81,7 @@ class _AddContactViewState extends State<AddContactView> {
return TabBar(
tabs: [
Tab(
icon: Icon(Icons.person_add_rounded),
icon: Icon(CwtchIcons.add_peer),
text: AppLocalizations.of(context)!.addPeer,
),
],
@ -92,11 +93,11 @@ class _AddContactViewState extends State<AddContactView> {
return TabBar(
tabs: [
Tab(
icon: Icon(Icons.person_add_rounded),
icon: Icon(CwtchIcons.add_peer),
text: AppLocalizations.of(context)!.tooltipAddContact,
),
//Tab(icon: Icon(Icons.backup), text: AppLocalizations.of(context)!.titleManageServers),
Tab(icon: Icon(Icons.group), text: AppLocalizations.of(context)!.createGroup),
Tab(icon: Icon(CwtchIcons.add_group), text: AppLocalizations.of(context)!.createGroup),
],
);
}
@ -118,7 +119,11 @@ class _AddContactViewState extends State<AddContactView> {
CwtchButtonTextField(
controller: ctrlrOnion,
onPressed: _copyOnion,
icon: Icon(Icons.copy),
readonly: true,
icon: Icon(
CwtchIcons.address_copy_2,
size: 32,
),
tooltip: AppLocalizations.of(context)!.copyBtn,
),
SizedBox(
@ -162,7 +167,7 @@ class _AddContactViewState extends State<AddContactView> {
Widget addGroupTab() {
// TODO We should replace with with a "Paste in Server Key Bundle"
if (Provider.of<ProfileInfoState>(context).serverList.servers.isEmpty) {
return Text("You need to add a server before you can create a group.");
return Text(AppLocalizations.of(context)!.addServerFirst);
}
return Container(

View File

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -11,6 +12,8 @@ import 'package:cwtch/widgets/textfield.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../cwtch_icons_icons.dart';
import '../errorHandler.dart';
import '../main.dart';
import '../opaque.dart';
import '../settings.dart';
@ -99,6 +102,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
),
CwtchTextField(
controller: ctrlrNick,
autofocus: false,
labelText: AppLocalizations.of(context)!.yourDisplayName,
validator: (value) {
if (value.isEmpty) {
@ -121,7 +125,11 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
CwtchButtonTextField(
controller: ctrlrOnion,
onPressed: _copyOnion,
icon: Icon(Icons.copy),
readonly: true,
icon: Icon(
CwtchIcons.address_copy_2,
size: 32,
),
tooltip: AppLocalizations.of(context)!.copyBtn,
)
])),
@ -147,7 +155,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
visible: usePassword,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty && Provider.of<ProfileInfoState>(context).isEncrypted,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
SizedBox(
@ -157,9 +165,12 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
controller: ctrlrOldPass,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
if (Provider.of<ProfileInfoState>(context).isEncrypted && Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (Provider.of<ErrorHandler>(context).deleteProfileError == true) {
return AppLocalizations.of(context)!.enterCurrentPasswordForDelete;
}
return null;
},
),
@ -167,7 +178,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
height: 20,
),
])),
CwtchLabel(label: AppLocalizations.of(context)!.password1Label),
CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
SizedBox(
height: 20,
),
@ -231,11 +242,9 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
Tooltip(
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
child: ElevatedButton.icon(
onPressed: checkCurrentPassword()
? null
: () {
showAlertDialog(context);
},
onPressed: () {
showAlertDialog(context);
},
style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()),
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn),
@ -301,52 +310,49 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
}
}
// TODO Stub - wire this into a libCwtch call.
bool checkCurrentPassword() {
return ctrlrOldPass.value.text.isEmpty;
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
onPressed: () {
var onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteProfile(onion, ctrlrOldPass.value.text);
Future.delayed(
const Duration(milliseconds: 500),
() {
if (globalErrorHandler.deleteProfileSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteProfileSuccess + ":" + onion));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.of(context).popUntil((route) => route.isFirst); // dismiss dialog
} else {
Navigator.of(context).pop();
}
},
);
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text("Cancel"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
onPressed: () {
// TODO Actually Delete the Peer
Navigator.of(context).pop(); // dismiss dialog
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}

View File

@ -1,3 +1,4 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/views/torstatusview.dart';
import 'package:cwtch/widgets/contactrow.dart';
@ -47,7 +48,7 @@ class _ContactsViewState extends State<ContactsView> {
width: 10,
),
Expanded(
child: Text("%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname).replaceAll("%2", "Contacts"),
child: Text("%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname).replaceAll("%2", AppLocalizations.of(context)!.titleManageContacts),
overflow: TextOverflow.ellipsis, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor()))), //todo
])),
actions: [
@ -66,7 +67,7 @@ class _ContactsViewState extends State<ContactsView> {
floatingActionButton: FloatingActionButton(
onPressed: _pushAddContact,
tooltip: AppLocalizations.of(context)!.tooltipAddContact,
child: const Icon(Icons.person_add_sharp),
child: const Icon(CwtchIcons.person_add_alt_1_24px),
),
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList());
}

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
import 'contactsview.dart';
import 'messageview.dart';
@ -14,24 +15,25 @@ class DoubleColumnView extends StatefulWidget {
class _DoubleColumnViewState extends State<DoubleColumnView> {
@override
Widget build(BuildContext context) {
var flwtch = Provider.of<FlwtchState>(context);
var flwtch = Provider.of<AppState>(context);
var cols = Provider.of<Settings>(context).uiColumns(true);
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
flex: flwtch.columns[0],
flex: cols[0],
child: ContactsView(
key: widget.key,
),
),
Flexible(
flex: flwtch.columns[1],
child: flwtch.selectedConversation == ""
? Center(child: Text("pick a contact"))
flex: cols[1],
child: flwtch.selectedConversation == null
? Card(child:Center(child: Text(AppLocalizations.of(context)!.addContactFirst)))
: //dev
MultiProvider(providers: [
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation)!),
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)!),
], child: Container(child: MessageView())),
),
],

View File

@ -1,4 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/opaque.dart';
@ -45,7 +47,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
child: Column(children: [
ListTile(
title: Text(AppLocalizations.of(context)!.settingLanguage, style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.language, color: settings.current().mainTextColor()),
leading: Icon(CwtchIcons.change_language, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: Provider.of<Settings>(context).locale.languageCode,
onChanged: (String? newValue) {
@ -62,7 +64,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
}).toList())),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingTheme, style: TextStyle(color: settings.current().mainTextColor())),
value: settings.current() == Opaque.light,
value: settings.current().identifier() == "light",
onChanged: (bool value) {
if (value) {
settings.setLight();
@ -73,26 +75,38 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.lightbulb_outline, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor()),
),
ListTile(
title: Text(/*AppLocalizations.of(context)!.settingLanguage*/ "UI Columns", style: TextStyle(color: settings.current().mainTextColor())),
title: Text(/*AppLocalizations.of(context)!.settingLanguage*/ "UI Columns in Portrait Mode", style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: "Single",
value: settings.uiColumnModePortrait.toString(),
onChanged: (String? newValue) {
if (newValue == "Double (1:2)") {
Provider.of<FlwtchState>(context).columns = [1, 2];
} else if (newValue == "Double (1:4)") {
Provider.of<FlwtchState>(context).columns = [1, 4];
} else {
Provider.of<FlwtchState>(context).columns = [1];
}
settings.uiColumnModePortrait = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: ["Single", "Double (1:2)", "Double (1:4)"].map<DropdownMenuItem<String>>((String value) {
items: Settings.uiColumnModeOptions(false).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
value: value.toString(),
child: Text(Settings.uiColumnModeToString(value)),
);
}).toList())),
ListTile(
title: Text(/*AppLocalizations.of(context)!.settingLanguage*/ "UI Columns in Landscape Mode", style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: settings.uiColumnModeLandscape.toString(),
onChanged: (String? newValue) {
settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(true).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(Settings.uiColumnModeToString(value)),
);
}).toList())),
SwitchListTile(
@ -109,7 +123,9 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.app_blocking, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.block_unknown, color: settings.current().mainTextColor()),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.experimentsEnabled, style: TextStyle(color: settings.current().mainTextColor())),
@ -124,7 +140,9 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.science, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.enable_experiments, color: settings.current().mainTextColor()),
),
Visibility(
visible: settings.experimentsEnabled,
@ -133,7 +151,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
SwitchListTile(
title: Text(AppLocalizations.of(context)!.enableGroups, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.descriptionExperimentsGroups),
value: settings.experiments.containsKey(TapirGroupsExperiment) && settings.experiments[TapirGroupsExperiment]!,
value: settings.isExperimentEnabled(TapirGroupsExperiment),
onChanged: (bool value) {
if (value) {
settings.enableExperiment(TapirGroupsExperiment);
@ -143,19 +161,15 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.group_sharp, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
),
],
)),
AboutListTile(
icon: Icon(Icons.info, color: settings.current().mainTextColor()),
applicationIcon: Padding(
padding: EdgeInsets.all(20),
child: Image(
image: AssetImage("assets/knott.png"),
width: 128,
height: 128,
)),
applicationIcon: Padding(padding:EdgeInsets.all(5), child: Icon(CwtchIcons.cwtch_knott)),
applicationName: "Cwtch (Flutter UI)",
applicationVersion: AppLocalizations.of(context)!.versionBuilddate.replaceAll("%1", EnvironmentConfig.BUILD_VER).replaceAll("%2", EnvironmentConfig.BUILD_DATE),
applicationLegalese: '\u{a9} 2021 Open Privacy Research Society',

View File

@ -1,3 +1,4 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/model.dart';
import 'package:cwtch/widgets/buttontextfield.dart';
@ -135,13 +136,13 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.rejectGroupBtn,
message: AppLocalizations.of(context)!.leaveGroup,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(Icons.delete),
label: Text(AppLocalizations.of(context)!.deleteBtn),
icon: Icon(CwtchIcons.leave_group),
label: Text(AppLocalizations.of(context)!.leaveGroup),
))
])
])))));
@ -158,7 +159,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text("Cancel"),
child: Text(AppLocalizations.of(context)!.cancel),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
@ -166,7 +167,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
);
Widget continueButton = TextButton(
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
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;
@ -179,7 +180,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
title: Text(AppLocalizations.of(context)!.reallyLeaveThisGroupPrompt),
actions: [
cancelButton,
continueButton,

View File

@ -1,5 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/views/peersettingsview.dart';
@ -40,17 +43,32 @@ class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context);
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
appBar: AppBar(
title: Text(Provider.of<ContactInfoState>(context).nickname),
// 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,
title: Row(children: [
ProfileImage(
imagePath: Provider.of<ContactInfoState>(context).imagePath,
diameter: 42,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor(),
badgeTextColor: Colors.red,
badgeColor: Colors.red,
),
SizedBox(
width: 10,
),Text(Provider.of<ContactInfoState>(context).nickname)]),
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: Icon(Icons.settings), onPressed: _pushContactSettings),
IconButton(icon: Icon(Icons.bug_report_outlined), onPressed: _debugResetContact),
//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),
],
),
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList()),
@ -63,12 +81,6 @@ class _MessageViewState extends State<MessageView> {
return true;
}
void _debugResetContact() {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.DebugResetContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion);
}
void _pushContactSettings() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext bcontext) {
@ -126,7 +138,7 @@ class _MessageViewState extends State<MessageView> {
child: TextFormField(
key: Key('txtCompose'),
controller: ctrlrCompose,
autofocus: true,
autofocus: !Platform.isAndroid,
focusNode: focusNode,
textInputAction: TextInputAction.send,
onFieldSubmitted: _sendMessage,
@ -135,12 +147,15 @@ class _MessageViewState extends State<MessageView> {
focusedBorder: InputBorder.none,
enabled: true,
prefixIcon: IconButton(
icon: Icon(Icons.insert_invitation, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: "Send a contact or group invite",
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(Icons.send, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: "Send Message",
icon: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: AppLocalizations.of(context)!.sendMessage,
onPressed: _sendMessage,
),
))),
@ -174,7 +189,9 @@ class _MessageViewState extends State<MessageView> {
),
ChangeNotifierProvider.value(
value: Provider.of<ProfileInfoState>(ctx, listen: false),
child: DropdownContacts(onChanged: (newVal) {
child: DropdownContacts(filter: (contact) {
return contact.onion != Provider.of<ContactInfoState>(context).onion;
}, onChanged: (newVal) {
setState(() {
this.selectedContact = newVal;
});
@ -185,7 +202,9 @@ class _MessageViewState extends State<MessageView> {
ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn),
onPressed: () {
this._sendInvitation();
if (this.selectedContact != "") {
this._sendInvitation();
}
Navigator.pop(bcontext);
},
),

View File

@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/model.dart';
import 'package:cwtch/widgets/buttontextfield.dart';
@ -76,8 +77,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
// todo translations
final snackBar = SnackBar(content: Text("Nickname changed successfully"));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.nickChangeSuccess));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
icon: Icon(Icons.save),
@ -138,12 +138,14 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
}
},
secondary: Icon(Icons.block, color: settings.current().mainTextColor()),
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.block_peer, color: settings.current().mainTextColor()),
),
ListTile(
title: Text(AppLocalizations.of(context)!.savePeerHistory, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.savePeerHistoryDescription),
leading: Icon(Icons.history_sharp, color: settings.current().mainTextColor()),
leading: Icon(CwtchIcons.peer_history, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: Provider.of<ContactInfoState>(context).savePeerHistory == "DefaultDeleteHistory"
? AppLocalizations.of(context)!.dontSavePeerHistory
@ -184,6 +186,22 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
);
}).toList())),
]),
Column(mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
Tooltip(
message: AppLocalizations.of(context)!.leaveGroup,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(CwtchIcons.leave_chat),
label: Text(AppLocalizations.of(context)!.leaveGroup),
))
])
]),
])))));
});
});
@ -194,4 +212,44 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text("Cancel"),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
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);
Future.delayed(Duration(milliseconds: 500), () {
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
});
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.reallyLeaveThisGroupPrompt),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}

View File

@ -1,16 +1,20 @@
import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
import 'package:cwtch/views/torstatusview.dart';
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:cwtch/widgets/tor_icon.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:cwtch/widgets/profilerow.dart';
import 'package:provider/provider.dart';
import '../config.dart';
import '../main.dart';
import '../model.dart';
import '../opaque.dart';
import '../torstatus.dart';
import 'addeditprofileview.dart';
import 'globalsettingsview.dart';
@ -24,6 +28,8 @@ class ProfileMgrView extends StatefulWidget {
class _ProfileMgrViewState extends State<ProfileMgrView> {
final ctrlrPassword = TextEditingController();
bool closeApp = false;
@override
void dispose() {
ctrlrPassword.dispose();
@ -36,57 +42,111 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
// Prevents Android back button from closing the app on the profile manager screen
// (which would shutdown connections and all kinds of other expensive to generate things)
// TODO pop up a dialogue regarding closing the app?
builder: (context, settings, child) => WillPopScope(
onWillPop: () async => false,
child: Scaffold(
backgroundColor: settings.theme.backgroundMainColor(),
appBar: AppBar(
title: Row(children: [
Image(
image: AssetImage("assets/core/knott-white.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
width: 32,
height: 32,
colorBlendMode: BlendMode.dstIn,
color: Provider.of<Settings>(context).theme.backgroundHilightElementColor(),
builder: (context, settings, child) =>
WillPopScope(
onWillPop: () async {
_showShutdown();
return closeApp;
},
child: Scaffold(
backgroundColor: settings.theme.backgroundMainColor(),
appBar: AppBar(
title: Row(children: [
Image(
image: AssetImage("assets/core/knott-white.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
width: 32,
height: 32,
colorBlendMode: BlendMode.dstIn,
color: Provider
.of<Settings>(context)
.theme
.backgroundHilightElementColor(),
),
SizedBox(
width: 10,
),
Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor())))
]),
actions: getActions(),
),
SizedBox(
width: 10,
floatingActionButton: FloatingActionButton(
onPressed: _pushAddEditProfile,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
),
),
Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor())))
]),
actions: [
IconButton(icon: TorIcon(), onPressed: _pushTorStatus),
IconButton(icon: Icon(Icons.bug_report_outlined), onPressed: _setLoggingLevelDebug),
IconButton(
icon: Icon(Icons.lock_open),
tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
onPressed: _modalUnlockProfiles,
),
IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddEditProfile,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
),
),
body: _buildProfileManager(),
)),
body: _buildProfileManager(),
)),
);
}
void _setLoggingLevelDebug() {
final setLoggingLevel = {
"EventType": "SetLoggingLevel",
"Data": {"Debug": "true"},
};
final setLoggingLevelJson = jsonEncode(setLoggingLevel);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendAppEvent(setLoggingLevelJson);
List<Widget> getActions() {
List<Widget> actions = new List<Widget>.empty(growable: true);
// Tor Status
actions.add(IconButton(
icon: TorIcon(),
onPressed: _pushTorStatus,
tooltip: Provider.of<TorStatus>(context).progress == 100
? AppLocalizations.of(context)!.networkStatusOnline
: (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor),
));
// Only show debug button on development builds
// Unlock Profiles
actions.add(IconButton(
icon: Icon(CwtchIcons.lock_open_24px),
tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
onPressed: _modalUnlockProfiles,
));
// Global Settings
actions.add(IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings));
actions.add(IconButton(icon: Icon(Icons.close), tooltip: AppLocalizations.of(context)!.shutdownCwtchTooltip, onPressed: _showShutdown));
return actions;
}
_showShutdown() {
// set up the buttons
Widget cancelButton = TextButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
child: Text(AppLocalizations.of(context)!.shutdownCwtchAction),
onPressed: () {
// Directly call the shutdown command, Android will do this for us...
Provider.of<FlwtchState>(context, listen: false).shutdown(MethodCall(""));
closeApp = true;
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.shutdownCwtchDialogTitle),
content: Text(AppLocalizations.of(context)!.shutdownCwtchDialog),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return alert;
},
);
}
void _pushGlobalSettings() {

View File

@ -1,11 +1,37 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import '../settings.dart';
class SplashView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Scaffold(
body: const Center(child: const Text("Loading Cwtch...")),
);
return Consumer<AppState>(
builder: (context, appState, child) => Scaffold(
body: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [
Image(
image: AssetImage("assets/core/knott-white.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
width: 200,
height: 200,
),
Image(
image: AssetImage("assets/cwtch_title.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Text(appState.appError == "" ? "Loading Cwtch..." : appState.appError,
style: TextStyle(
fontSize: 16.0, color: appState.appError == "" ? Provider.of<Settings>(context).theme.mainTextColor() : Provider.of<Settings>(context).theme.textfieldErrorColor())),
),
Image(image: AssetImage("assets/Open_Privacy_Logo_lightoutline.png")),
])),
));
}
}

View File

@ -23,7 +23,7 @@ class _TorStatusView extends State<TorStatusView> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Tor Network Status"),
title: Text(AppLocalizations.of(context)!.torNetworkStatus),
),
body: _buildSettingsList(),
);
@ -43,15 +43,19 @@ class _TorStatusView extends State<TorStatusView> {
child: Column(children: [
ListTile(
leading: TorIcon(),
title: Text("Tor Status"),
title: Text(AppLocalizations.of(context)!.torStatus),
subtitle: Text(torStatus.progress == 100 ? AppLocalizations.of(context)!.networkStatusOnline : torStatus.status),
trailing: ElevatedButton(
child: Text("Reset"),
child: Text(AppLocalizations.of(context)!.resetTor),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.ResetTor();
},
),
)
),
ListTile(
title: Text(AppLocalizations.of(context)!.torVersion),
subtitle: Text(torStatus.version),
),
]))));
});
});

View File

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

View File

@ -3,6 +3,10 @@ import 'package:provider/provider.dart';
import '../model.dart';
bool noFilter(ContactInfoState peer) {
return true;
}
// Dropdown menu populated from Provider.of<ProfileInfoState>'s contact list
// Includes both peers and groups; begins empty/nothing selected
// Displays nicknames to UI but uses handles as values
@ -10,8 +14,10 @@ import '../model.dart';
class DropdownContacts extends StatefulWidget {
DropdownContacts({
required this.onChanged,
this.filter = noFilter,
});
final Function(dynamic) onChanged;
final bool Function(ContactInfoState) filter;
@override
_DropdownContactsState createState() => _DropdownContactsState();
@ -24,7 +30,7 @@ class _DropdownContactsState extends State<DropdownContacts> {
Widget build(BuildContext context) {
return DropdownButton(
value: this.selected,
items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.map<DropdownMenuItem<String>>((ContactInfoState contact) {
items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.where(widget.filter).map<DropdownMenuItem<String>>((ContactInfoState contact) {
return DropdownMenuItem<String>(value: contact.onion, child: Text(contact.nickname));
}).toList(),
onChanged: (String? newVal) {

View File

@ -17,6 +17,18 @@ class CwtchButtonTextField extends StatefulWidget {
}
class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
late final FocusNode _focusNode;
@override
void initState() {
_focusNode = FocusNode();
_focusNode.addListener(() {
// Select all...
if (_focusNode.hasFocus) widget.controller.selection = TextSelection(baseOffset: 0, extentOffset: widget.controller.text.length);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Consumer<Settings>(builder: (context, theme, child) {
@ -24,10 +36,12 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
controller: widget.controller,
readOnly: widget.readonly,
showCursor: !widget.readonly,
focusNode: _focusNode,
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: widget.onPressed,
icon: widget.icon,
padding: EdgeInsets.fromLTRB(0.0, 4.0, 2.0, 2.0),
tooltip: widget.tooltip,
enableFeedback: true,
color: theme.current().mainTextColor(),

View File

@ -20,6 +20,8 @@ class _ContactRowState extends State<ContactRow> {
var contact = Provider.of<ContactInfoState>(context);
return Card(
clipBehavior: Clip.antiAlias,
color: Provider.of<AppState>(context).selectedConversation == contact.onion ? Provider.of<Settings>(context).theme.backgroundHilightElementColor() : null,
borderOnForeground: false,
child: InkWell(
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Padding(
@ -31,7 +33,7 @@ class _ContactRowState extends State<ContactRow> {
diameter: 64.0,
imagePath: contact.imagePath,
maskOut: !contact.isOnline(),
border: contact.isOnline() ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
border: contact.isOnline() ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
),
Expanded(
child: Padding(
@ -41,16 +43,19 @@ class _ContactRowState extends State<ContactRow> {
children: [
Text(
contact.nickname, //(contact.isInvitation ? "invite " : "non-invite ") + (contact.isBlocked ? "blokt" : "nonblokt"),//
style: Provider.of<FlwtchState>(context).biggerFont,
style: TextStyle(fontSize: Provider.of<Settings>(context).theme.contactOnionTextSize(),
color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor() : Provider.of<Settings>(context).theme.mainTextColor()), //Provider.of<FlwtchState>(context).biggerFont,
softWrap: true,
overflow: TextOverflow.visible,
),
Text(contact.onion),
Text(contact.onion,
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor() : Provider.of<Settings>(context).theme.mainTextColor())),
],
))),
Padding(
padding: const EdgeInsets.all(5.0),
child: contact.isInvitation != null && contact.isInvitation
child: contact.isInvitation == true
? Wrap(direction: Axis.vertical, children: <Widget>[
IconButton(
padding: EdgeInsets.zero,
@ -77,17 +82,19 @@ class _ContactRowState extends State<ContactRow> {
]),
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.setState(() => flwtch.selectedConversation = contact.onion);
// case 2/3 handled by Double/TripleColumnView respectively
if (flwtch.columns.length == 1) _pushMessageView(contact.onion);
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contact.onion)!.unreadMessages = 0;
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).selectedConversation = contact.onion;
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(contact.onion);
});
},
));
}
void _pushMessageView(String handle) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push(
MaterialPageRoute<void>(
@ -132,6 +139,6 @@ class _ContactRowState extends State<ContactRow> {
return DateFormat.yMd().format(date.toLocal());
}
// Otherwise just state the time.
return DateFormat.Hms().format(date.toLocal());
return DateFormat.Hm().format(date.toLocal());
}
}

View File

@ -4,7 +4,6 @@ import '../settings.dart';
// Provides a styled Label
// Callers must provide a label text
// TODO: Integrate this with a settings "zoom" / accessibility setting
class CwtchLabel extends StatefulWidget {
CwtchLabel({required this.label});
final String label;

View File

@ -1,16 +1,19 @@
import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/widgets/malformedbubble.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
// todo: Reject buttons currently aren't tracked and will reset when the message is reloaded
class InvitationBubble extends StatefulWidget {
@override
InvitationBubbleState createState() => InvitationBubbleState();
@ -18,15 +21,21 @@ class InvitationBubble extends StatefulWidget {
class InvitationBubbleState extends State<InvitationBubble> {
bool rejected = false;
FocusNode _focus = FocusNode();
bool isAccepted = false;
@override
Widget build(BuildContext context) {
if (Provider.of<MessageState>(context).malformed) {
return MalformedBubble();
}
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
var isGroup = Provider.of<MessageState>(context).overlay == 101;
var isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget) != null;
isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget) != null;
var prettyDate = "";
var borderRadiousEh = 15.0;
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
rejected = Provider.of<MessageState>(context).flags & 0x01 == 0x01;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
if (Provider.of<MessageState>(context).timestamp != null) {
@ -50,56 +59,36 @@ class InvitationBubbleState extends State<InvitationBubble> {
child: SelectableText(senderDisplayStr + '\u202F',
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor())));
// todo: translations
var messageStr = "";
if (fromMe) {
//todo: get group name?
messageStr = "You sent an invitation for " + (isGroup ? "a group" : Provider.of<MessageState>(context).message ?? "");
} else {
messageStr = (isGroup ? "You have been invited to join " + (Provider.of<MessageState>(context).inviteNick ?? "") : "This is a contact suggestion for:") +
"\n" +
(Provider.of<MessageState>(context).inviteTarget ?? "");
// If we receive an invite for ourselves, treat it as a bug. The UI no longer allows this so it could have only come from
// some kind of malfeasance.
var selfInvite = Provider.of<MessageState>(context).inviteNick == Provider.of<ProfileInfoState>(context).onion;
if (selfInvite) {
return MalformedBubble();
}
var wdgMessage = Center(
widthFactor: 1,
child: SelectableText(
messageStr + '\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 = isGroup && !showGroupInvite ?
Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) :
fromMe
? senderInviteChrome(AppLocalizations.of(context)!.sendAnInvitation,
isGroup ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget)!.nickname : Provider.of<MessageState>(context).message, myKey)
: (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, Provider.of<MessageState>(context).inviteNick,
Provider.of<MessageState>(context).inviteTarget, myKey));
Widget wdgDecorations;
if (fromMe) {
wdgDecorations = Center(
widthFactor: 1.0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(prettyDate,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
textAlign: fromMe ? TextAlign.right : TextAlign.left),
!fromMe
? SizedBox(width: 1, height: 1)
: Provider.of<MessageState>(context).ackd
? Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
],
));
if (isGroup && !showGroupInvite) {
wdgDecorations = Text('\u202F');
} else if (fromMe) {
wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate);
} else if (isAccepted) {
wdgDecorations = Text("Accepted!");
wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F');
} else if (this.rejected) {
wdgDecorations = Text("Rejected.");
wdgDecorations = Text(AppLocalizations.of(context)!.rejected + '\u202F');
} else {
wdgDecorations = Center(
widthFactor: 1,
child: Row(children: [
Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Reject"), onPressed: _btnReject)),
Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Accept"), onPressed: _btnAccept)),
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)),
]));
}
@ -123,35 +112,83 @@ class InvitationBubbleState extends State<InvitationBubble> {
widthFactor: 1.0,
child: Padding(
padding: EdgeInsets.all(9.0),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(4), child: Icon(Icons.group_add, size: 32))),
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(isGroup && !showGroupInvite ? CwtchIcons.enable_experiments : CwtchIcons.send_invite, size: 32))),
Center(
widthFactor: 1.0,
child: Column(
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])),
widthFactor: 1.0,
child: Column(
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]),
)
])))));
});
}
void _btnReject() {
//todo: how should we track inline invite rejections?
setState(() => this.rejected = true);
setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
var idx = Provider.of<MessageState>(context, listen: false).messageIndex;
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageState>(context, listen: false).flags | 0x01);
Provider.of<MessageState>(context).flags |= 0x01;
});
}
void _btnAccept() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
if (Provider.of<MessageState>(context, listen: false).overlay == 100) {
final setPeerAttribute = {
"EventType": "AddContact",
"Data": {"ImportString": Provider.of<MessageState>(context, listen: false).message},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
} else {
setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, Provider.of<MessageState>(context, listen: false).message);
}
isAccepted = true;
});
}
// Construct an invite chrome for the sender
Widget senderInviteChrome(String chrome, String targetName, String myKey) {
return Wrap(children: [
SelectableText(
chrome + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,
),
SelectableText(
targetName + '\u202F',
key: Key(myKey),
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,
)
]);
}
// Construct an invite chrome
Widget inviteChrome(String chrome, String targetName, String targetId, String myKey) {
return Wrap(children: [
SelectableText(
chrome + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
maxLines: 2,
),
SelectableText(
targetName + '\u202F',
key: Key(myKey),
style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,
)
]);
}
}

View File

@ -1,10 +1,6 @@
import 'dart:convert';
import 'dart:ffi';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../settings.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
final Color malformedColor = Color(0xFFE85DA1);
@ -40,22 +36,17 @@ class MalformedBubbleState extends State<MalformedBubble> {
widthFactor: 1,
child: Padding(
padding: EdgeInsets.all(4),
child: Image(
image: AssetImage("assets/core/broken_heart_24.png"),
filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: BlendMode.srcIn,
color: Provider.of<Settings>(context).theme.mainTextColor(),
isAntiAlias: false,
width: 32,
height: 32))),
child: Icon(
CwtchIcons.favorite_black_24dp_broken,
size: 24,
))),
Center(
widthFactor: 1.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [Text("Malformed Message")],
children: [Text(AppLocalizations.of(context)!.malformedMessage)],
))
])))));
});

View File

@ -1,9 +1,11 @@
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import 'package:intl/intl.dart';
import '../settings.dart';
import 'messagebubbledecorations.dart';
class MessageBubble extends StatefulWidget {
@override
@ -50,26 +52,9 @@ class MessageBubbleState extends State<MessageBubble> {
textWidthBasis: TextWidthBasis.longestLine,
);
var wdgDecorations = Center(
widthFactor: 1.0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(prettyDate,
style: TextStyle(
fontSize: 9.0,
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: fromMe ? TextAlign.right : TextAlign.left),
!fromMe
? SizedBox(width: 1, height: 1)
: Provider.of<MessageState>(context).ackd == true
? Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: (Provider.of<MessageState>(context).error == true
? Icon(Icons.error_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12))
],
));
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var error = Provider.of<MessageState>(context).error;
return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString());
@ -77,9 +62,14 @@ class MessageBubbleState extends State<MessageBubble> {
child: Container(
child: Container(
decoration: BoxDecoration(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(),
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
border: Border.all(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(), width: 1),
color: error
? malformedColor
: (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),

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../settings.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// Provides message decorations (acks/errors/dates etc.) for generic message bubble overlays (chats, invites etc.)
class MessageBubbleDecoration extends StatefulWidget {
MessageBubbleDecoration({required this.ackd, required this.errored, required this.prettyDate, required this.fromMe});
final String prettyDate;
final bool fromMe;
final bool ackd;
final bool errored;
@override
_MessageBubbleDecoration createState() => _MessageBubbleDecoration();
}
class _MessageBubbleDecoration extends State<MessageBubbleDecoration> {
@override
Widget build(BuildContext context) {
return Center(
widthFactor: 1.0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.prettyDate,
style:
TextStyle(fontSize: 9.0, color: widget.fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
textAlign: widget.fromMe ? TextAlign.right : TextAlign.left),
!widget.fromMe
? SizedBox(width: 1, height: 1)
: Padding(
padding: EdgeInsets.all(1.0),
child: widget.ackd == true
? Tooltip(
message: AppLocalizations.of(context)!.acknowledgedLabel,
child: Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))
: (widget.errored == true
? Tooltip(
message: AppLocalizations.of(context)!.couldNotSendMsgError,
child: Icon(Icons.error_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))
: Tooltip(
message: AppLocalizations.of(context)!.pendingLabel,
child: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))))
],
));
;
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../model.dart';
import '../settings.dart';
import 'messagerow.dart';
@ -15,24 +16,50 @@ class _MessageListState extends State<MessageList> {
@override
Widget build(BuildContext outerContext) {
bool showEphemeralWarning = (Provider.of<ContactInfoState>(context).isGroup == false && Provider.of<ContactInfoState>(context).savePeerHistory != "SaveHistory");
bool showOfflineWarning = Provider.of<ContactInfoState>(context).isOnline() == false;
bool showMessageWarning = showEphemeralWarning || showOfflineWarning;
bool showSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status != "Synced";
return RepaintBoundary(
child: Container(
child: Scrollbar(
isAlwaysShown: true,
controller: ctrlr1,
child: Container(
child: Column(children: [
Visibility(
visible: showMessageWarning,
child: Container(
padding: EdgeInsets.all(5.0),
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
child: showSyncing ?
Text(AppLocalizations.of(context)!.serverNotSynced,
textAlign: TextAlign.center)
: showOfflineWarning
? Text(Provider.of<ContactInfoState>(context).isGroup ? AppLocalizations.of(context)!.serverConnectivityDisconnected : AppLocalizations.of(context)!.peerOfflineMessage,
textAlign: TextAlign.center)
// Only show the ephemeral status for peer conversations, not for groups...
: (showEphemeralWarning
? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center)
:
// We are not allowed to put null here, so put an empty text widge
Text("")),
)),
Expanded(
child: Scrollbar(
controller: ctrlr1,
child: Container(
// Only show broken heart is the contact is offline...
decoration: BoxDecoration(
image: Provider.of<ContactInfoState>(outerContext).isOnline()
? null
: DecorationImage(
fit: BoxFit.contain,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
image: AssetImage("assets/core/negative_heart_512px.png"),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.mainTextColor(), BlendMode.srcIn))),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementTextColor(), BlendMode.srcIn))),
// Don't load messages for syncing server...
child: ListView.builder(
controller: ctrlr1,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true,
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 trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
return ChangeNotifierProvider(
@ -51,7 +78,7 @@ class _MessageListState extends State<MessageList> {
return RepaintBoundary(child: MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)));
});
},
),
))));
))))
])));
}
}

View File

@ -31,10 +31,8 @@ class _MessageRowState extends State<MessageRow> {
fromMe = false;
}
Widget wdgBubble = Flexible(
flex: 3,
fit: FlexFit.loose,
child: malformed ? MalformedBubble() : (Provider.of<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble()));
Widget wdgBubble =
Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble());
Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Provider.of<Settings>(context).theme.dropShadowColor());
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
var widgetRow = <Widget>[];
@ -96,7 +94,10 @@ class _MessageRowState extends State<MessageRow> {
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact));
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.successfullAddedContact),
duration: Duration(seconds: 2),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}

View File

@ -1,11 +1,13 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../settings.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// Provides a styled Password Input Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator.
class CwtchPasswordField extends StatefulWidget {
CwtchPasswordField({required this.controller, required this.validator, this.action, this.autofocus = true});
CwtchPasswordField({required this.controller, required this.validator, this.action, this.autofocus = false});
final TextEditingController controller;
final FormFieldValidator validator;
final Function(String)? action;
@ -21,9 +23,9 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
@override
Widget build(BuildContext context) {
// todo: translations
var label = "View Password";
var label = AppLocalizations.of(context)!.tooltipShowPassword;
if (!obscureText) {
label = "Hide Password";
label = AppLocalizations.of(context)!.tooltipHidePassword;
}
return Consumer<Settings>(builder: (context, theme, child) {
@ -32,6 +34,7 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
controller: widget.controller,
validator: widget.validator,
obscureText: obscureText,
autovalidateMode: AutovalidateMode.always,
onFieldSubmitted: widget.action,
textInputAction: TextInputAction.unspecified,
enableSuggestions: false,
@ -43,7 +46,7 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
obscureText = !obscureText;
});
},
icon: Icon((obscureText ? Icons.remove_red_eye : Icons.remove_red_eye_outlined), semanticLabel: label),
icon: Icon((obscureText ? CwtchIcons.eye_closed : CwtchIcons.eye_open), semanticLabel: label),
tooltip: label,
color: theme.current().mainTextColor(),
highlightColor: theme.current().defaultButtonColor(),

View File

@ -38,7 +38,7 @@ class _ProfileImageState extends State<ProfileImage> {
filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: !widget.maskOut
? Provider.of<Settings>(context).theme == Opaque.dark
? Provider.of<Settings>(context).theme.identifier() == "dark"
? BlendMode.softLight
: BlendMode.darken
: BlendMode.srcOut,

View File

@ -30,85 +30,96 @@ class _ProfileRowState extends State<ProfileRow> {
padding: const EdgeInsets.all(2.0), //border size
child: ProfileImage(
badgeCount: 0,
badgeColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor(),
badgeTextColor: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor(),
badgeColor: Provider
.of<Settings>(context)
.theme
.portraitProfileBadgeColor(),
badgeTextColor: Provider
.of<Settings>(context)
.theme
.portraitProfileBadgeTextColor(),
diameter: 64.0,
imagePath: profile.imagePath,
border: profile.isOnline ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor())),
border: profile.isOnline ? Provider
.of<Settings>(context)
.theme
.portraitOnlineBorderColor() : Provider
.of<Settings>(context)
.theme
.portraitOfflineBorderColor())),
Expanded(
child: Column(
children: [
Text(
profile.nickname,
semanticsLabel: profile.nickname,
style: Provider.of<FlwtchState>(context).biggerFont,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
ExcludeSemantics(
child: Text(
profile.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
))
],
)),
children: [
Text(
profile.nickname,
semanticsLabel: profile.nickname,
style: Provider
.of<FlwtchState>(context)
.biggerFont,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
ExcludeSemantics(
child: Text(
profile.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
))
],
)),
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context)!.editProfile + " " + profile.nickname,
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () {
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath);
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted);
},
)
],
),
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.cwtch.SelectProfile(profile.onion);
flwtch.setState(() {
flwtch.selectedProfile = profile;
flwtch.selectedConversation = "";
});
var appState = Provider.of<AppState>(context, listen: false);
appState.selectedProfile = profile.onion;
appState.selectedConversation = null;
switch (flwtch.columns.length) {
case 1:
_pushContactList(profile, false);
break;
case 2:
_pushContactList(profile, true);
break;
} // case 3: handled by TripleColumnView
_pushContactList(profile, appState.isLandscape(context));//orientation == Orientation.landscape);
});
},
));
}
void _pushContactList(ProfileInfoState profile, bool includeDoublePane) {
void _pushContactList(ProfileInfoState profile, bool isLandscape) {
Navigator.of(context).push(
MaterialPageRoute<void>(
settings: RouteSettings(name: "conversations"),
builder: (BuildContext buildcontext) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>.value(value: profile),
ChangeNotifierProvider<ContactListState>.value(value: profile.contactList),
],
builder: (context, widget) => includeDoublePane ? DoubleColumnView() : ContactsView(),
);
return OrientationBuilder(
builder: (orientationBuilderContext, orientation) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>.value(value: profile),
ChangeNotifierProvider<ContactListState>.value(value: profile.contactList),
],
builder: (innercontext, widget) {
var appState = Provider.of<AppState>(context);
var settings = Provider.of<Settings>(context);
return settings.uiColumns(appState.isLandscape(innercontext)).length > 1 ? DoubleColumnView() : ContactsView();
}
);
});
},
),
);
}
void _pushAddEditProfile({onion: "", displayName: "", profileImage: ""}) {
void _pushAddEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>(
create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage),
create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage, encrypted: encrypted),
),
],
builder: (context, widget) => AddEditProfileView(),

View File

@ -7,17 +7,30 @@ doNothing(String x) {}
// Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator.
class CwtchTextField extends StatefulWidget {
CwtchTextField({required this.controller, required this.labelText, this.validator = null, this.onChanged = doNothing});
CwtchTextField({required this.controller, required this.labelText, this.validator, this.autofocus = false, this.onChanged = doNothing});
final TextEditingController controller;
final String labelText;
final FormFieldValidator? validator;
final Function(String) onChanged;
final bool autofocus;
@override
_CwtchTextFieldState createState() => _CwtchTextFieldState();
}
class _CwtchTextFieldState extends State<CwtchTextField> {
late final FocusNode _focusNode;
@override
void initState() {
_focusNode = FocusNode();
_focusNode.addListener(() {
// Select all...
if (_focusNode.hasFocus) widget.controller.selection = TextSelection(baseOffset: 0, extentOffset: widget.controller.text.length);
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Consumer<Settings>(builder: (context, theme, child) {
@ -25,6 +38,8 @@ class _CwtchTextFieldState extends State<CwtchTextField> {
controller: widget.controller,
validator: widget.validator,
onChanged: widget.onChanged,
autofocus: widget.autofocus,
focusNode: _focusNode,
decoration: InputDecoration(
labelText: widget.labelText,
labelStyle: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),

View File

@ -1,3 +1,4 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -16,13 +17,9 @@ class _TorIconState extends State<TorIcon> {
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: Image(
image: AssetImage(Provider.of<TorStatus>(context).progress == 0
? "assets/core/Tor_OFF.png"
: (Provider.of<TorStatus>(context).progress == 100 ? "assets/core/Tor_icon.png" : "assets/core/Tor_Booting_up.png")),
// Color the onion per the text color...
child: Icon(
Provider.of<TorStatus>(context).progress == 0 ? CwtchIcons.onion_off : (Provider.of<TorStatus>(context).progress == 100 ? CwtchIcons.onion_on : CwtchIcons.onion_waiting),
color: Provider.of<Settings>(context).theme.mainTextColor(),
colorBlendMode: BlendMode.srcIn,
semanticLabel: Provider.of<TorStatus>(context).progress == 100
? AppLocalizations.of(context)!.networkStatusOnline
: (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor),

10
linux/cwtch.home.desktop Executable file
View File

@ -0,0 +1,10 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Cwtch
Comment=Metadata Resistant Chat
Exec=env LD_LIBRARY_PATH=~/.local/lib/cwtch/ ~/.local/bin/cwtch
Icon=cwtch
Terminal=false
Categories=Network;InstantMessaging;
Keywords=Internet;IM;Instant Messaging;Messaging;Chat;

View File

@ -1,9 +1,10 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=cwtch
Name=Cwtch
Comment=Metadata Resistant Chat
Exec=env LD_LIBRARY_PATH=./lib/ ./cwtch
Icon=cwtch
Terminal=false
Categories=Internet;Chat;
Categories=Network;InstantMessaging;
Keywords=Internet;IM;Instant Messaging;Messaging;Chat

10
linux/cwtch.sys.desktop Executable file
View File

@ -0,0 +1,10 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Cwtch
Comment=Metadata Resistant Chat
Exec=env LD_LIBRARY_PATH=/usr/lib/cwtch /usr/bin/cwtch
Icon=cwtch
Terminal=false
Categories=Network;InstantMessaging;
Keywords=Internet;IM;Instant Messaging;Messaging;Chat

View File

@ -2,12 +2,10 @@
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <window_size/window_size_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) window_size_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin");
window_size_plugin_register_with_registrar(window_size_registrar);
}

View File

@ -2,6 +2,8 @@
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_

View File

@ -3,7 +3,6 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
window_size
)
set(PLUGIN_BUNDLED_LIBRARIES)

17
linux/install-home.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
mkdir -p ~/.local/bin
cp cwtch ~/.local/bin/
mkdir -p ~/.local/share/icons
cp cwtch.png ~/.local/share/icons
mkdir -p ~/.local/share/cwtch
cp -r data ~/.local/share/cwtch
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

13
linux/install-sys.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
cp cwtch /usr/bin/
cp cwtch.png /usr/share/icons
mkdir -p /usr/share/cwtch
cp -r data /usr/share/cwtch
mkdir -p /usr/lib/cwtch
cp -r lib/* /usr/lib/cwtch
cp cwtch.sys.desktop /usr/share/applications/cwtch.desktop

View File

@ -1,5 +1,14 @@
#include "my_application.h"
// Added to check for location of assets folder
#include <sys/types.h>
#include <sys/stat.h>
// To get the home dir of the user
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
@ -12,6 +21,18 @@ struct _MyApplication {
char** dart_entrypoint_arguments;
};
// Redefining from flutter/engine::shell/platform/linux/fl_dart_project.cc
// struct def required here to enable compiler to allow access to variables
struct _FlDartProject {
GObject parent_instance;
gboolean enable_mirrors;
gchar* aot_library_path;
gchar* assets_path;
gchar* icu_data_path;
gchar** dart_entrypoint_args;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
@ -48,11 +69,36 @@ static void my_application_activate(GApplication* application) {
gtk_window_set_title(window, "cwtch");
}
gtk_window_set_icon_from_file(window, "./cwtch.png", NULL);
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
// Check if assets folder is relative to the executable or if we can use a system copy
struct stat info;
if (stat(fl_dart_project_get_assets_path(project), &info ) != 0 ) {
if( stat("/usr/share/cwtch/data/flutter_assets", &info ) != 0 ) {
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
// /home/$USER/.local/share/cwtch/data/flutter_assets
project->assets_path = g_build_filename(homedir, ".local", "share", "cwtch", "data", "flutter_assets", nullptr);
// /home/$USER/.local/lib/cwtch/
project->aot_library_path = g_build_filename(homedir, ".local", "lib", "cwtch", "libapp.so", nullptr);
// /home/$USER/.local/share/cwtch/data
project->icu_data_path = g_build_filename(homedir, ".local", "share", "cwtch", "data", "icudtl.dat", nullptr);
gtk_window_set_icon_from_file(window, g_build_filename(homedir, ".local", "share", "icons", "cwtch.png", nullptr), NULL);
} else {
// /usr/share/cwtch/data/flutter_assets
project->assets_path = g_build_filename("/", "usr", "share", "cwtch", "data", "flutter_assets", nullptr);
// /usr/lib/cwtch
project->aot_library_path = g_build_filename("/", "usr", "lib", "cwtch", "libapp.so", nullptr);
// /usr/share/cwtch/data
project->icu_data_path = g_build_filename("/", "usr", "share", "cwtch", "data", "icudtl.dat", nullptr);
gtk_window_set_icon_from_file(window, "/usr/share/icons/cwtch.png", NULL);
}
} else {
gtk_window_set_icon_from_file(window, "./cwtch.png", NULL);
}
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);

View File

@ -1,27 +1,27 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
ansicolor:
dependency: transitive
description:
name: archive
name: ansicolor
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.2"
version: "2.0.1"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.1"
version: "2.7.0"
boolean_selector:
dependency: transitive
description:
@ -42,7 +42,7 @@ packages:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.1"
clock:
dependency: transitive
description:
@ -57,13 +57,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
cupertino_icons:
dependency: "direct main"
description:
@ -77,7 +70,7 @@ packages:
name: dbus
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
version: "0.5.1"
desktop_notifications:
dependency: "direct main"
description:
@ -111,11 +104,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_driver:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -131,18 +119,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: "direct main"
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "1.2.0"
http:
dependency: transitive
description:
@ -157,13 +140,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
integration_test:
dependency: "direct main"
injector:
dependency: transitive
description:
name: integration_test
name: injector
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2+3"
version: "2.0.0"
intl:
dependency: transitive
description:
@ -191,7 +174,14 @@ packages:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.4.0"
msix:
dependency: "direct dev"
description:
name: msix
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
nested:
dependency: transitive
description:
@ -199,6 +189,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
package_info_plus:
dependency: "direct main"
description:
@ -358,13 +369,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
sync_http:
dependency: transitive
description:
name: sync_http
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
term_glyph:
dependency: transitive
description:
@ -378,7 +382,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
version: "0.4.1"
typed_data:
dependency: transitive
description:
@ -393,20 +397,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "6.2.0"
webdriver:
dependency: transitive
description:
name: webdriver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
win32:
dependency: transitive
description:
@ -414,15 +404,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
window_size:
dependency: "direct main"
description:
path: "plugins/window_size"
ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81
resolved-ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81
url: "git://github.com/google/flutter-desktop-embedding.git"
source: git
version: "0.1.0"
xdg_directories:
dependency: transitive
description:
@ -436,7 +417,14 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
version: "5.1.2"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
sdks:
dart: ">=2.13.0 <3.0.0"
flutter: ">=1.20.0"

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
version: 1.0.0+15
environment:
sdk: ">=2.12.0 <3.0.0"
@ -37,30 +37,18 @@ dependencies:
desktop_notifications: 0.5.0
glob: any
# todo: flutter_driver causes version conflict. eg https://github.com/flutter/flutter/issues/44829
# testing-related deps
integration_test: ^1.0.0
flutter_test:
sdk: flutter
flutter_driver:
sdk: flutter
window_size:
git:
url: git://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81
dev_dependencies:
msix: ^2.1.3
# Uncomment to update lokalise translations (see README for list of deps to comment out bc incompatibilities)
#dev_dependencies:
# flutter_lokalise: any
# alternatively: flutter pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/intl/app_localizations.dart lib/l10n/intl_*.arb --api-token X --project-id Y
#flutter_lokalise:
# project_id: ""
# api_token: ""
# include_tags:
# - tag1
# - tag2
# project_id: "737094205fceda35c50aa2.60364948"
# api_token: "0407300fe4aa1edf1c1818e56234589e74c83c59" # Read only api Token from Dan
flutter_intl:
enabled: true
@ -95,6 +83,11 @@ flutter:
- assets/profiles/
- assets/servers/
fonts:
- family: CwtchIcons
fonts:
- asset: assets/fonts/CwtchIcons.ttf
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
@ -114,3 +107,19 @@ flutter:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
msix_config:
display_name: Cwtch
publisher_display_name: Open Privacy Research Society
identity_name: im.cwtch.flwtch
msix_version: 1.0.0.0
certificate_path: codesign.pfx
certificate_password: pfx_pass
publisher: CN=Open Privacy Research Society, O=Open Privacy Research Society, L=Vancouver, S=British Columbia, C=CA
logo_path: cwtch.png
start_menu_icon_path: cwtch.png
tile_icon_path: assets\cwtch_title.png
icons_background_color: transparent
architecture: x64
capabilities: 'internetClient'

View File

@ -14,8 +14,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
var settingsEnglishDark = Settings(Locale("en", ''), OpaqueDark());
var settingsEnglishLight = Settings(Locale("en", ''), OpaqueLight());
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
void main() {

View File

@ -14,8 +14,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
var settingsEnglishDark = Settings(Locale("en", ''), OpaqueDark());
var settingsEnglishLight = Settings(Locale("en", ''), OpaqueLight());
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
void main() {

View File

@ -14,8 +14,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
var settingsEnglishDark = Settings(Locale("en", ''), OpaqueDark());
var settingsEnglishLight = Settings(Locale("en", ''), OpaqueLight());
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
String file(String slug) {

View File

@ -14,8 +14,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
var settingsEnglishDark = Settings(Locale("en", ''), OpaqueDark());
var settingsEnglishLight = Settings(Locale("en", ''), OpaqueLight());
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
String file(String slug) {

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.15)
project(flutter_app LANGUAGES CXX)
project(cwtch LANGUAGES CXX)
set(BINARY_NAME "flutter_app")
set(BINARY_NAME "cwtch")
cmake_policy(SET CMP0063 NEW)

BIN
windows/nsis/brand_side.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

6
windows/nsis/create.sh Normal file
View File

@ -0,0 +1,6 @@
- cp nsis/cwtch-installer.nsi deploy/
- cd deploy
- makensis -V3 cwtch-installer.nsi
- export BUILDDATE=`date +%G-%m-%d-%H-%M`
- export FILENAME=cwtch-installer-$BUILDDATE.exe
- mv cwtch-installer.exe $FILENAME

View File

@ -0,0 +1,92 @@
; USAGE: Run in ui/deploy, requires the output be in 'windows' directory
!include "MUI2.nsh"
; General settings ----------------------------
Name "Cwtch"
; !define MUI_BRANDINGTEXT "SIG Beta Ver. 1.0"
Unicode True
# define the name of the installer
Outfile "cwtch-installer.exe"
# For removing Start Menu shortcut in Windows 7
#RequestExecutionLevel user
RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on)
# define the directory to install to, the desktop in this case as specified
# by the predefined $DESKTOP variable
InstallDir "$PROGRAMFILES\Cwtch"
;Get installation folder from registry if available
InstallDirRegKey HKCU "Software\Cwtch" "installLocation"
; MUI Interface -----------------------------
!define MUI_INSTALLCOLORS "DFB9DE 281831"
; 128x128, 32bit
!define MUI_ICON "../runner/resources/knot_128.ico"
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "cwtch_title.bmp"
!define MUI_TEXTCOLOR "350052"
!define MUI_WELCOMEFINISHPAGE_BITMAP "brand_side.bmp"
!define MUI_WELCOMEFINISHPAGE_BITMAP_STRETCH NoStretchNoCrop
!define MUI_INSTFILESPAGE_COLORS "DFB9DE 281831"
!define MUI_INSTFILESPAGE_PROGRESSBAR "colored"
!define MUI_FINISHPAGE_NOAUTOCLOSE
ShowInstDetails show
; Pages --------
!define MUI_WELCOMEPAGE_TITLE "Welcome to the Cwtch installer"
!define MUI_WELCOMEPAGE_TEXT "Cwtch (pronounced: kutch) is a Welsh word roughly meaning 'a hug that creates a safe space'$\n$\n\
Cwtch is a platform for building consentful, decentralized, untrusted infrastructure using metadata resistant group communication applications. Currently there is a selfnamed instant messaging prototype app that is driving development and testing. Many Further apps are planned as the platform matures."
!define MUI_FINISHPAGE_TITLE "Enjoy Cwtch"
!define MUI_FINISHPAGE_RUN $INSTDIR/ui.exe
!define MUI_FINISHPAGE_TEXT "You can keep up-to-date on Cwtch and report any issues you have at https://cwtch.im"
!define MUI_FINISHPAGE_LINK "https://cwtch.im"
!define MUI_FINISHPAGE_LINK_LOCATION "https://cwtch.im"
!define MUI_FINISHPAGE_LINK_COLOR "D01972"
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "../../LICENSE"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
; Languages --------------------------------
!insertmacro MUI_LANGUAGE "English"
# default section
Section
# define the output path for this file
SetOutPath $INSTDIR
# define what to install and place it in the output path
# Filler for .sh to populate with contents of deploy/windows
#FILESLISTSTART
FILE /r "..\..\build\windows\runner\Release\"
#FILESLISTEND
# create a shortcut in the start menu programs directory
CreateDirectory "$SMPROGRAMS\Cwtch"
CreateShortcut "$SMPROGRAMS\Cwtch\Cwtch.lnk" "$INSTDIR\cwtch.exe" "" "$INSTDIR\cwtch.ico"
;Store installation folder
WriteRegStr HKCU "Software\Cwtch" "installLocation" $INSTDIR
SectionEnd

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -52,7 +52,16 @@ END
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
IDI_APP_ICON_LG ICON "resources\\knot_256.ico"
IDI_APP_ICON_SM ICON "resources\\knot_64.ico"
IDI_APP_ICON_256 ICON "resources\\knot_256.ico"
IDI_APP_ICON_128 ICON "resources\\knot_128.ico"
IDI_APP_ICON_64 ICON "resources\\knot_64.ico"
IDI_APP_ICON_48 ICON "resources\\knot_48.ico"
IDI_APP_ICON_32 ICON "resources\\knot_32.ico"
IDI_APP_ICON_16 ICON "resources\\knot_16.ico"
/////////////////////////////////////////////////////////////////////////////
@ -89,13 +98,13 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "A new Flutter project." "\0"
VALUE "CompanyName", "Open Privacy Research Society" "\0"
VALUE "FileDescription", "Cwtch Instant Messenger" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "flutter_app" "\0"
VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "flutter_app.exe" "\0"
VALUE "ProductName", "flutter_app" "\0"
VALUE "InternalName", "cwtch" "\0"
VALUE "LegalCopyright", "Copyright (C) 2021 Open Privacy Research Society. All rights reserved." "\0"
VALUE "OriginalFilename", "cwtch.exe" "\0"
VALUE "ProductName", "Cwtch" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END

View File

@ -30,7 +30,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(&run_loop, project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.CreateAndShow(L"flutter_app", origin, size)) {
if (!window.CreateAndShow(L"cwtch", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);

View File

@ -2,7 +2,16 @@
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
#define IDI_APP_ICON_LG 101
#define IDI_APP_ICON_SM 102
#define IDI_APP_ICON_256 103
#define IDI_APP_ICON_128 104
#define IDI_APP_ICON_64 105
#define IDI_APP_ICON_48 106
#define IDI_APP_ICON_32 107
#define IDI_APP_ICON_16 108
// Next default values for new objects
//

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

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