Merge pull request 'UI Fix Bulk' (#96) from ui_fixes into trunk

Reviewed-on: flutter/flutter_app#96
This commit is contained in:
erinn 2021-05-28 15:55:20 -07:00
commit acd7fc8259
58 changed files with 1365 additions and 649 deletions

View File

@ -1 +1 @@
v0.0.2-36-g84d85b7-2021-05-20-01-14
v0.0.2-43-ga98b5de-2021-05-28-21-21

View File

@ -46,7 +46,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
and then use:
```
Text(AppLocalizations.of(context).stringIdentifer),
Text(AppLocalizations.of(context)!.stringIdentifer),
```
### Configuration

13
SPEC.md
View File

@ -129,3 +129,16 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [ ] Leave Group
- [ ] Pressing Back should go back to the message pane for the group
## Android Requirements Notes
What are our expectations here?
- Can we periodically check groups in the background to power notifications?
- Either way we need networking in the service not the main/UI thread.
- We probably don't want to and very likely can't persist tor connections to peers indefinitely.
- Neither google nor apple are very tolerant of apps that try to create their own push message infrastructure.
- "Aside": Retrieving a CallbackHandle for a method from PluginUtilities.getCallbackHandle has the side effect of populating a callback cache within the Flutter engine, as seen in the diagram above. This cache maps information required to retrieve callbacks to raw integer handles, which are simply hashes calculated based on the properties of the callback. This cache persists across launches, but be aware that callback lookups may fail if the callback is renamed or moved and PluginUtilities.getCallbackHandle is not called for the updated callback.
- The above seems to imply that there is a persistent cache somewhere that can affect code between launches...the ramifications of this are ?!?!

View File

@ -8,6 +8,7 @@
<application
android:name="io.flutter.app.FlutterApplication"
android:label="Cwtch"
android:extractNativeLibs="true"
android:icon="@mipmap/knott">
<activity
android:name=".MainActivity"

View File

@ -178,6 +178,17 @@ class MainActivity: FlutterActivity() {
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) ?: "";

View File

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

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/core/knott-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
cwtch-android-lifecycle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
cwtch.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -26,24 +26,29 @@ abstract class Cwtch {
void DebugResetContact(String profileOnion, String contactHandle);
// ignore: non_constant_identifier_names
Future<String> ACNEvents();
Future<dynamic> ACNEvents();
// ignore: non_constant_identifier_names
Future<String> ContactEvents();
Future<dynamic> ContactEvents();
// ignore: non_constant_identifier_names
Future<String> GetProfiles();
Future<dynamic> GetProfiles();
// ignore: non_constant_identifier_names
Future<int> NumMessages(String profile, String handle);
Future<dynamic> NumMessages(String profile, String handle);
// ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index);
Future<dynamic> GetMessage(String profile, String handle, int index);
// ignore: non_constant_identifier_names
Future<String> GetMessages(String profile, String handle, int start, int end);
Future<dynamic> GetMessages(String profile, String handle, int start, int end);
// 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 CreateGroup(String profile, String server, String groupName);
// ignore: non_constant_identifier_names
void LeaveGroup(String profile, String groupID);
// ignore: non_constant_identifier_names
void ImportBundle(String profile, String bundle);
// ignore: non_constant_identifier_names

View File

@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:cwtch/notification_manager.dart';
import 'package:provider/provider.dart';
import 'package:cwtch/torstatus.dart';
@ -10,16 +11,18 @@ import '../settings.dart';
// Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin)
// Takes Notifiers and triggers them on appropriate events
class CwtchNotifier {
ProfileListState profileCN;
Settings settings;
ErrorHandler error;
TorStatus torStatus;
late ProfileListState profileCN;
late Settings settings;
late ErrorHandler error;
late TorStatus torStatus;
late NotificationsManager notificationManager;
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN) {
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP) {
profileCN = pcn;
settings = settingsCN;
error = errorCN;
torStatus = torStatusCN;
notificationManager = notificationManagerP;
}
void handleMessage(String type, dynamic data) {
@ -29,7 +32,7 @@ class CwtchNotifier {
onion: data["Identity"], nickname: data["name"], imagePath: data["picture"], contactsJson: data["ContactsJson"], serversJson: data["ServerList"], online: data["Online"] == "true"));
break;
case "PeerCreated":
profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState(
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(
data["ProfileOnion"],
data["RemotePeer"],
nickname: data["nick"],
@ -45,8 +48,18 @@ class CwtchNotifier {
lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
));
break;
case "GroupCreated":
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"], server: data["GroupServer"], isGroup: true, lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
}
break;
case "DeleteGroup":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["GroupID"]);
break;
case "PeerStateChange":
ContactInfoState contact = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]);
ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]);
if (contact != null) {
if (data["ConnectionState"] != null) {
contact.status = data["ConnectionState"];
@ -56,13 +69,14 @@ class CwtchNotifier {
contact.isBlocked = data["authorization"] == "blocked";
}
// contact.[status/isBlocked] might change the list's sort order
profileCN.getProfile(data["ProfileOnion"]).contactList.resort();
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
}
break;
case "NewMessageFromPeer":
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());
notificationManager.notify("New Message From Peer!");
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;
case "PeerAcknowledgement":
// We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end...
@ -71,10 +85,10 @@ class CwtchNotifier {
var idx = data["Index"];
// We return -1 for protocol message acks if there is no message
if (idx == "-1") break;
var key = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).getMessageKey(idx);
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx);
if (key == null) break;
try {
var message = Provider.of<MessageState>(key.currentContext, listen: false);
var message = Provider.of<MessageState>(key.currentContext!, listen: false);
if (message == null) break;
message.ackd = true;
} catch (e) {
@ -85,16 +99,16 @@ class CwtchNotifier {
case "NewMessageFromGroup":
if (data["ProfileOnion"] != data["RemotePeer"]) {
//not from me
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());
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 {
// from me (already displayed - do not update counter)
var idx = data["Signature"];
var key = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).getMessageKey(idx);
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx);
if (key == null) break;
try {
var message = Provider.of<MessageState>(key.currentContext, listen: false);
var message = Provider.of<MessageState>(key.currentContext!, listen: false);
if (message == null) break;
message.ackd = true;
} catch (e) {
@ -102,14 +116,25 @@ class CwtchNotifier {
}
}
break;
case "IndexedFailure":
print("IndexedFailure: $data");
var idx = data["Index"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx);
try {
var message = Provider.of<MessageState>(key!.currentContext!, listen: false);
message.error = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
break;
case "SendMessageToGroupError":
// from me (already displayed - do not update counter)
print("SendMessageToGroupError: $data");
var idx = data["Signature"];
var key = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).getMessageKey(idx);
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx);
if (key == null) break;
try {
var message = Provider.of<MessageState>(key.currentContext, listen: false);
var message = Provider.of<MessageState>(key.currentContext!, listen: false);
if (message == null) break;
message.error = true;
} catch (e) {
@ -118,28 +143,30 @@ class CwtchNotifier {
break;
case "AppError":
print("New App Error: $data");
error.handleUpdate(data["Data"]);
if (data["Data"] != null) {
error.handleUpdate(data["Data"]);
}
break;
case "UpdateGlobalSettings":
settings.handleUpdate(jsonDecode(data["Data"]));
break;
case "SetAttribute":
if (data["Key"] == "public.name") {
profileCN.getProfile(data["ProfileOnion"]).nickname = data["Data"];
profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"];
} else {
print("unhandled set attribute event: $type");
print("unhandled set attribute event: $type $data");
}
break;
case "NetworkError":
var isOnline = data["Status"] == "Success";
profileCN.getProfile(data["ProfileOnion"]).isOnline = isOnline;
profileCN.getProfile(data["ProfileOnion"])?.isOnline = isOnline;
break;
case "ACNStatus":
print("acn status: $data");
torStatus.handleUpdate(int.parse(data["Progress"]), data["Status"]);
break;
case "UpdateServerInfo":
profileCN.getProfile(data["ProfileOnion"]).replaceServers(data["ServerList"]);
profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]);
break;
case "NewGroup":
print("new group invite: $data");
@ -148,30 +175,38 @@ class CwtchNotifier {
String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5)));
dynamic groupInvite = jsonDecode(inviteJson);
print("new group invite: $groupInvite");
if (profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"],
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"],
isInvitation: true, imagePath: data["PicturePath"], nickname: groupInvite["GroupName"], server: groupInvite["ServerHost"], isGroup: true, lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(groupInvite["GroupID"], DateTime.now());
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(groupInvite["GroupID"], DateTime.now());
}
}
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());
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.isInvitation = false;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
break;
case "ServerStateChange":
print("server state change: $data");
profileCN.getProfile(data["ProfileOnion"]).contactList.contacts.forEach((contact) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) {
if (contact.isGroup == true && contact.server == data["GroupServer"]) {
contact.status = data["ConnectionState"];
}
});
profileCN.getProfile(data["ProfileOnion"]).contactList.resort();
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
break;
case "SetGroupAttribute":
if (data["Key"] == "local.name") {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.nickname = data["Data"];
}
} else {
print("unhandled set group attribute event: $type $data");
}
break;
default:
print("unhandled event: $type");
print("unhandled event: $type $data");
}
}
}

View File

@ -56,9 +56,9 @@ typedef acn_events_function = Pointer<Utf8> Function();
typedef ACNEventsFn = Pointer<Utf8> Function();
class CwtchFfi implements Cwtch {
DynamicLibrary library;
CwtchNotifier cwtchNotifier;
Isolate cwtchIsolate;
late DynamicLibrary library;
late CwtchNotifier cwtchNotifier;
late Isolate cwtchIsolate;
CwtchFfi(CwtchNotifier _cwtchNotifier) {
if (Platform.isWindows) {
@ -79,9 +79,9 @@ class CwtchFfi implements Cwtch {
String bundledTor = "";
Map<String, String> envVars = Platform.environment;
if (Platform.isLinux) {
home = envVars['HOME'];
home = (envVars['HOME'])!;
} else if (Platform.isWindows) {
home = envVars['UserProfile'];
home = (envVars['UserProfile'])!;
bundledTor = "Tor\\Tor\\tor.exe";
}
var cwtchDir = path.join(home, ".cwtch/dev/");
@ -121,7 +121,7 @@ class CwtchFfi implements Cwtch {
// Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it
static Stream<String> pollAppbusEvents() async* {
DynamicLibrary library;
late DynamicLibrary library;
if (Platform.isWindows) {
library = DynamicLibrary.open("libCwtch.dll");
} else if (Platform.isLinux) {
@ -356,4 +356,26 @@ class CwtchFfi implements Cwtch {
final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length);
}
@override
void CreateGroup(String profileOnion, String server, String groupName) {
var createGroup = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_CreateGroup");
// ignore: non_constant_identifier_names
final CreateGroup = createGroup.asFunction<VoidFromStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = server.toNativeUtf8();
final u3 = groupName.toNativeUtf8();
CreateGroup(u1, u1.length, u2, u2.length, u3, u3.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 u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length);
}
}

View File

@ -26,9 +26,9 @@ class CwtchGomobile implements Cwtch {
final appbusEventChannelName = 'test.flutter.dev/eventBus';
Future<String> androidLibraryDir;
Future<Directory> androidHomeDirectory;
CwtchNotifier cwtchNotifier;
late Future<dynamic> androidLibraryDir;
late Future<dynamic> androidHomeDirectory;
late CwtchNotifier cwtchNotifier;
CwtchGomobile(CwtchNotifier _cwtchNotifier) {
print("gomobile.dart: CwtchGomobile()");
@ -73,34 +73,34 @@ class CwtchGomobile implements Cwtch {
}
// ignore: non_constant_identifier_names
Future<String> ACNEvents() {
Future<dynamic> ACNEvents() {
return cwtchPlatform.invokeMethod("ACNEvents");
}
// ignore: non_constant_identifier_names
Future<String> ContactEvents() {
Future<dynamic> ContactEvents() {
return cwtchPlatform.invokeMethod("ContactEvents");
}
// ignore: non_constant_identifier_names
Future<String> GetProfiles() {
Future<dynamic> GetProfiles() {
print("gomobile.dart: GetProfiles()");
return cwtchPlatform.invokeMethod("GetProfiles");
}
// ignore: non_constant_identifier_names
Future<int> NumMessages(String profile, String handle) {
Future<dynamic> NumMessages(String profile, String handle) {
return cwtchPlatform.invokeMethod("NumMessages", {"profile": profile, "contact": handle});
}
// ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index) {
Future<dynamic> GetMessage(String profile, String handle, int index) {
print("gomobile.dart GetMessage " + index.toString());
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index});
}
// ignore: non_constant_identifier_names
Future<String> GetMessages(String profile, String handle, int start, int end) {
Future<dynamic> GetMessages(String profile, String handle, int start, int end) {
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "start": start, "end": end});
}
@ -172,4 +172,15 @@ class CwtchGomobile implements Cwtch {
void RejectInvite(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "handle": groupHandle});
}
@override
void CreateGroup(String profileOnion, String server, String groupName) {
cwtchPlatform.invokeMethod("CreateGroup", {"ProfileOnion": profileOnion, "server": server, "groupName": groupName});
}
@override
// ignore: non_constant_identifier_names
void LeaveGroup(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "handle": groupHandle});
}
}

View File

@ -1,3 +1,4 @@
import 'package:cwtch/notification_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:cwtch/cwtch/ffi.dart';
import 'package:cwtch/cwtch/gomobile.dart';
@ -35,14 +36,14 @@ class Flwtch extends StatefulWidget {
class FlwtchState extends State<Flwtch> {
final TextStyle biggerFont = const TextStyle(fontSize: 18);
Cwtch cwtch;
late Cwtch cwtch;
bool cwtchInit = false;
ProfileInfoState selectedProfile;
late ProfileInfoState selectedProfile;
String selectedConversation = "";
var columns = [1]; // default or 'single column' mode
//var columns = [1, 1, 2];
AppModel appStatus;
ProfileListState profs;
late AppModel appStatus;
late ProfileListState profs;
@override
initState() {
@ -50,11 +51,15 @@ class FlwtchState extends State<Flwtch> {
cwtchInit = false;
profs = ProfileListState();
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus);
if (Platform.isAndroid) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager());
cwtch = CwtchGomobile(cwtchNotifier);
} else if (Platform.isLinux) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, LinuxNotificationsManager());
cwtch = CwtchFfi(cwtchNotifier);
} else {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager());
cwtch = CwtchFfi(cwtchNotifier);
}
@ -76,19 +81,24 @@ class FlwtchState extends State<Flwtch> {
@override
Widget build(BuildContext context) {
//appStatus = AppModel(cwtch: cwtch);
globalSettings.initPackageInfo();
return MultiProvider(
providers: [getFlwtchStateProvider(), getProfileListProvider(), getSettingsProvider(), getErrorHandlerProvider(), getTorStatusProvider()],
providers: [
getFlwtchStateProvider(),
getProfileListProvider(),
getSettingsProvider(),
getErrorHandlerProvider(),
getTorStatusProvider(),
],
builder: (context, widget) {
Provider.of<Settings>(context).initPackageInfo();
return Consumer<Settings>(
builder: (context, opaque, child) => MaterialApp(
builder: (context, settings, child) => MaterialApp(
key: Key('app'),
locale: Provider.of<Settings>(context).locale,
locale: settings.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'Cwtch',
theme: mkThemeData(opaque),
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() : ProfileMgrView()) : SplashView(),

View File

@ -63,6 +63,6 @@ class DiskAssetBundle extends CachingAssetBundle {
@override
Future<ByteData> load(String key) async {
return _cache[key];
return _cache[key]!;
}
}

View File

@ -14,33 +14,11 @@ import 'main.dart';
/// UI State ///
////////////////////
//todo: delete
class ProfileModel {
String onion;
String nickname;
String creationDate;
String imagePath;
HashMap<String, ContactModel> contacts;
}
//todo: delete
class ContactModel {
String onion;
String nickname;
bool isGroup;
bool isInvitation;
bool isBlocked;
String status;
String imagePath;
ContactModel({this.onion, this.nickname, this.status, this.isInvitation, this.isBlocked, this.imagePath});
}
class ChatMessage {
final int o;
final String d;
ChatMessage({this.o, this.d});
ChatMessage({required this.o, required this.d});
ChatMessage.fromJson(Map<String, dynamic> json)
: o = json['o'],
@ -72,7 +50,7 @@ class ProfileListState extends ChangeNotifier {
List<ProfileInfoState> get profiles => _profiles.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
ProfileInfoState getProfile(String onion) {
ProfileInfoState? getProfile(String onion) {
int idx = _profiles.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _profiles[idx] : null;
}
@ -80,10 +58,10 @@ class ProfileListState extends ChangeNotifier {
class ContactListState extends ChangeNotifier {
List<ContactInfoState> _contacts = [];
String _filter;
String _filter = "";
int get num => _contacts.length;
int get numFiltered => isFiltered ? filteredList().length : num;
bool get isFiltered => _filter != null && _filter != "";
bool get isFiltered => _filter != "";
String get filter => _filter;
set filter(String newVal) {
_filter = newVal;
@ -92,7 +70,7 @@ class ContactListState extends ChangeNotifier {
List<ContactInfoState> filteredList() {
if (!isFiltered) return contacts;
return _contacts.where((ContactInfoState c) => c.onion.contains(_filter) || (c.nickname != null && c.nickname.contains(_filter))).toList();
return _contacts.where((ContactInfoState c) => c.onion.contains(_filter) || (c.nickname.contains(_filter))).toList();
}
void addAll(Iterable<ContactInfoState> newContacts) {
@ -140,10 +118,18 @@ class ContactListState extends ChangeNotifier {
List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
ContactInfoState getContact(String onion) {
ContactInfoState? getContact(String onion) {
int idx = _contacts.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _contacts[idx] : null;
}
void removeContact(String onion) {
int idx = _contacts.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_contacts.removeAt(idx);
notifyListeners();
}
}
}
class ProfileInfoState extends ChangeNotifier {
@ -156,7 +142,7 @@ class ProfileInfoState extends ChangeNotifier {
bool _online = false;
ProfileInfoState({
this.onion,
required this.onion,
nickname = "",
imagePath = "",
unreadMessages = 0,
@ -253,21 +239,21 @@ class ProfileInfoState extends ChangeNotifier {
class ContactInfoState extends ChangeNotifier {
final String profileOnion;
final String onion;
String _nickname;
late String _nickname;
bool _isInvitation;
bool _isBlocked;
String _status;
String _imagePath;
String _savePeerHistory;
int _unreadMessages = 0;
int _totalMessages = 0;
DateTime _lastMessageTime;
Map<String, GlobalKey> keys;
late bool _isInvitation;
late bool _isBlocked;
late String _status;
late String _imagePath;
late String _savePeerHistory;
late int _unreadMessages = 0;
late int _totalMessages = 0;
late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageBubbleState>> keys;
// todo: a nicer way to model contacts, groups and other "entities"
bool _isGroup;
String _server;
late bool _isGroup;
late String _server;
ContactInfoState(
this.profileOnion,
@ -293,14 +279,14 @@ class ContactInfoState extends ChangeNotifier {
this._totalMessages = numMessages;
this._unreadMessages = numUnread;
this._savePeerHistory = savePeerHistory;
this._lastMessageTime = lastMessageTime;
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
this._server = server;
keys = Map<String, GlobalKey>();
keys = Map<String, GlobalKey<MessageBubbleState>>();
}
get nickname => this._nickname;
String get nickname => this._nickname;
get savePeerHistory => this._savePeerHistory;
String get savePeerHistory => this._savePeerHistory;
set savePeerHistory(String newVal) {
this._savePeerHistory = newVal;
notifyListeners();
@ -311,49 +297,49 @@ class ContactInfoState extends ChangeNotifier {
notifyListeners();
}
get isGroup => this._isGroup;
bool get isGroup => this._isGroup;
set isGroup(bool newVal) {
this._isGroup = newVal;
notifyListeners();
}
get isBlocked => this._isBlocked;
bool get isBlocked => this._isBlocked;
set isBlocked(bool newVal) {
this._isBlocked = newVal;
notifyListeners();
}
get isInvitation => this._isInvitation;
bool get isInvitation => this._isInvitation;
set isInvitation(bool newVal) {
this._isInvitation = newVal;
notifyListeners();
}
get status => this._status;
String get status => this._status;
set status(String newVal) {
this._status = newVal;
notifyListeners();
}
get unreadMessages => this._unreadMessages;
int get unreadMessages => this._unreadMessages;
set unreadMessages(int newVal) {
this._unreadMessages = newVal;
notifyListeners();
}
get totalMessages => this._totalMessages;
int get totalMessages => this._totalMessages;
set totalMessages(int newVal) {
this._totalMessages = newVal;
notifyListeners();
}
get imagePath => this._imagePath;
String get imagePath => this._imagePath;
set imagePath(String newVal) {
this._imagePath = newVal;
notifyListeners();
}
get lastMessageTime => this._lastMessageTime;
DateTime get lastMessageTime => this._lastMessageTime;
set lastMessageTime(DateTime newVal) {
this._lastMessageTime = newVal;
notifyListeners();
@ -374,7 +360,8 @@ class ContactInfoState extends ChangeNotifier {
if (keys[index] == null) {
keys[index] = GlobalKey<MessageBubbleState>();
}
return keys[index];
GlobalKey<MessageBubbleState> ret = keys[index]!;
return ret;
}
}
@ -382,24 +369,24 @@ class MessageState extends ChangeNotifier {
final String profileOnion;
final String contactHandle;
final int messageIndex;
String _message;
int _overlay;
String _inviteTarget;
String _inviteNick;
DateTime _timestamp;
String _senderOnion;
String _senderImage;
String _signature = "";
bool _ackd = false;
bool _error = false;
bool _loaded = false;
bool _malformed = false;
late String _message;
late int _overlay;
late String _inviteTarget;
late String _inviteNick;
late DateTime _timestamp;
late String _senderOnion;
late String _senderImage;
late String _signature = "";
late bool _ackd = false;
late bool _error = false;
late bool _loaded = false;
late bool _malformed = false;
MessageState({
BuildContext context,
this.profileOnion,
this.contactHandle,
this.messageIndex,
required BuildContext context,
required this.profileOnion,
required this.contactHandle,
required this.messageIndex,
}) {
this._senderOnion = profileOnion;
tryLoad(context);
@ -408,8 +395,8 @@ class MessageState extends ChangeNotifier {
get message => this._message;
get overlay => this._overlay;
get timestamp => this._timestamp;
get ackd => this._ackd;
get error => this._error;
bool get ackd => this._ackd;
bool get error => this._error;
get malformed => this._malformed;
get senderOnion => this._senderOnion;
get senderImage => this._senderImage;
@ -444,7 +431,7 @@ class MessageState extends ChangeNotifier {
dynamic message = jsonDecode(messageWrapper['Message']);
this._message = message['d'];
this._overlay = int.parse(message['o'].toString());
this._timestamp = DateTime.tryParse(messageWrapper['Timestamp']);
this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
this._senderOnion = messageWrapper['PeerID'];
this._senderImage = messageWrapper['ContactImage'];
@ -490,7 +477,7 @@ class MessageState extends ChangeNotifier {
class AppModel {
final Cwtch cwtch;
AppModel({this.cwtch});
AppModel({required this.cwtch});
Stream<String> contactEvents() async* {
while (true) {

View File

@ -9,7 +9,7 @@ class ServerListState extends ChangeNotifier {
notifyListeners();
}
ServerInfoState getServer(String onion) {
ServerInfoState? getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null;
}
@ -22,5 +22,5 @@ class ServerInfoState extends ChangeNotifier {
final String onion;
final String status;
ServerInfoState({this.onion, this.status});
ServerInfoState({required this.onion, required this.status});
}

View File

@ -0,0 +1,26 @@
import 'package:desktop_notifications/desktop_notifications.dart';
import 'package:path/path.dart' as path;
// NotificationsManager provides a wrapper around platform specific notifications logic.
abstract class NotificationsManager {
Future<void> notify(String message);
}
// NullNotificationsManager ignores all notification requests
class NullNotificationsManager implements NotificationsManager {
@override
Future<void> notify(String message) async {}
}
// LinuxNotificationsManager uses the desktop_notifications package to implement
// the standard dbus-powered linux desktop notifications.
class LinuxNotificationsManager implements NotificationsManager {
int previous_id = 0;
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

@ -690,7 +690,7 @@ class CwtchLight extends OpaqueThemeType {
}
Color textfieldBackgroundColor() {
return whitePurple;
return purple;
}
Color textfieldBorderColor() {
@ -722,11 +722,11 @@ class CwtchLight extends OpaqueThemeType {
}
Color portraitOnlineBorderColor() {
return darkPurple;
return greyPurple;
}
Color portraitOnlineBackgroundColor() {
return darkPurple;
return greyPurple;
}
Color portraitOnlineTextColor() {
@ -1225,7 +1225,7 @@ class Opaque extends OpaqueThemeType {
return sidePaneMinSize() + chatPaneMinSize();
}
static OpaqueThemeType _current;
static late OpaqueThemeType _current;
static final OpaqueThemeType dark = CwtchDark();
static final OpaqueThemeType light = CwtchLight();
static void setDark() {
@ -1345,16 +1345,26 @@ 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(),
accentColor: opaque.current().defaultButtonColor(),
buttonColor: opaque.current().defaultButtonColor(),
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()),
@ -1364,11 +1374,16 @@ ThemeData mkThemeData(Settings opaque) {
),
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))),
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()),
@ -1387,5 +1402,13 @@ ThemeData mkThemeData(Settings opaque) {
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

@ -14,12 +14,12 @@ const TapirGroupsExperiment = "tapir-groups-experiment";
/// Settings Pane.
class Settings extends ChangeNotifier {
Locale locale;
PackageInfo packageInfo;
late PackageInfo packageInfo;
OpaqueThemeType theme;
bool experimentsEnabled;
late bool experimentsEnabled;
HashMap<String, bool> experiments = HashMap.identity();
bool blockUnknownConnections;
late bool blockUnknownConnections;
/// Set the dark theme.
void setDark() {
@ -112,11 +112,13 @@ class Settings extends ChangeNotifier {
/// Turn on a specific experiment.
enableExperiment(String key) {
experiments.update(key, (value) => true, ifAbsent: () => true);
notifyListeners();
}
/// Turn off a specific experiment
disableExperiment(String key) {
experiments.update(key, (value) => false, ifAbsent: () => false);
notifyListeners();
}
/// Construct a default settings object.

View File

@ -5,6 +5,8 @@ class TorStatus extends ChangeNotifier {
String status;
bool connected;
TorStatus({this.connected = false, this.progress = 0, this.status = ""});
/// Called by the event bus.
handleUpdate(int new_progress, String new_status) {
if (progress == 100) {

View File

@ -26,7 +26,6 @@ class AddContactView extends StatefulWidget {
class _AddContactViewState extends State<AddContactView> {
final _formKey = GlobalKey<FormState>();
final _createGroupFormKey = GlobalKey<FormState>();
final _joinGroupFormKey = GlobalKey<FormState>();
final ctrlrOnion = TextEditingController(text: "");
final ctrlrContact = TextEditingController(text: "");
final ctrlrGroupName = TextEditingController(text: "");
@ -34,9 +33,14 @@ class _AddContactViewState extends State<AddContactView> {
@override
Widget build(BuildContext context) {
// if we haven't picked a server yet, pick the first one in the list...
if (server.isEmpty) {
server = Provider.of<ProfileInfoState>(context).serverList.servers.first.onion;
}
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).titleManageContacts),
title: Text(AppLocalizations.of(context)!.titleManageContacts),
),
body: _buildForm(),
);
@ -46,15 +50,20 @@ class _AddContactViewState extends State<AddContactView> {
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];
bool groupsEnabled = Provider.of<Settings>(context).experimentsEnabled && Provider.of<Settings>(context).experiments[TapirGroupsExperiment]!;
return Consumer<ErrorHandler>(builder: (context, globalErrorHandler, child) {
return DefaultTabController(
length: groupsEnabled ? 4 : 1,
length: groupsEnabled ? 2 : 1,
child: Column(children: [
(groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()),
Expanded(
child: TabBarView(
children: (groupsEnabled ? [addPeerTab(), manageServersTab(), addGroupTab(), joinGroupTab()] : [addPeerTab()]),
children: (groupsEnabled
? [
addPeerTab(),
addGroupTab(),
]
: [addPeerTab()]),
)),
]));
});
@ -62,7 +71,7 @@ class _AddContactViewState extends State<AddContactView> {
void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
@ -72,7 +81,7 @@ class _AddContactViewState extends State<AddContactView> {
tabs: [
Tab(
icon: Icon(Icons.person_add_rounded),
text: AppLocalizations.of(context).addPeer,
text: AppLocalizations.of(context)!.addPeer,
),
],
);
@ -84,11 +93,10 @@ class _AddContactViewState extends State<AddContactView> {
tabs: [
Tab(
icon: Icon(Icons.person_add_rounded),
text: AppLocalizations.of(context).addPeer,
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(Icons.group_add), text: AppLocalizations.of(context).joinGroup),
//Tab(icon: Icon(Icons.backup), text: AppLocalizations.of(context)!.titleManageServers),
Tab(icon: Icon(Icons.group), text: AppLocalizations.of(context)!.createGroup),
],
);
}
@ -103,7 +111,7 @@ class _AddContactViewState extends State<AddContactView> {
autovalidateMode: AutovalidateMode.always,
key: _formKey,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context).profileOnionLabel),
CwtchLabel(label: AppLocalizations.of(context)!.profileOnionLabel),
SizedBox(
height: 20,
),
@ -111,12 +119,12 @@ class _AddContactViewState extends State<AddContactView> {
controller: ctrlrOnion,
onPressed: _copyOnion,
icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context).copyBtn,
tooltip: AppLocalizations.of(context)!.copyBtn,
),
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).pasteAddressToAddContact),
CwtchLabel(label: AppLocalizations.of(context)!.pasteAddressToAddContact),
SizedBox(
height: 20,
),
@ -127,29 +135,25 @@ class _AddContactViewState extends State<AddContactView> {
return null;
}
if (globalErrorHandler.invalidImportStringError) {
return AppLocalizations.of(context).invalidImportString;
return AppLocalizations.of(context)!.invalidImportString;
} else if (globalErrorHandler.contactAlreadyExistsError) {
return AppLocalizations.of(context).contactAlreadyExists;
return AppLocalizations.of(context)!.contactAlreadyExists;
} else if (globalErrorHandler.explicitAddContactSuccess) {}
return null;
},
onChanged: (String peerAddr) async {
onChanged: (String importBundle) async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
final setPeerAttribute = {
"EventType": "AddContact",
"Data": {"ImportString": peerAddr},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, importBundle);
Future.delayed(const Duration(milliseconds: 500), () {
if (globalErrorHandler.explicitAddContactSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).successfullAddedContact + peerAddr));
if (globalErrorHandler.importBundleSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + importBundle));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context);
}
});
},
labelText: '',
)
])));
}
@ -161,11 +165,6 @@ class _AddContactViewState extends State<AddContactView> {
return Text("You need to add a server before you can create a group.");
}
// if we haven't picked a server yet, pick the first one in the list...
if (server.isEmpty) {
server = Provider.of<ProfileInfoState>(context).serverList.servers.first.onion;
}
return Container(
margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20),
@ -176,80 +175,59 @@ class _AddContactViewState extends State<AddContactView> {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CwtchLabel(label: AppLocalizations.of(context).server),
CwtchLabel(label: AppLocalizations.of(context)!.server),
SizedBox(
height: 20,
),
DropdownButton(
onChanged: (newServer) {
server = newServer;
onChanged: (String? newServer) {
setState(() {
server = newServer!;
});
},
isExpanded: true, // magic property
value: server,
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
return DropdownMenuItem<String>(
value: serverInfo.onion,
child: Text(serverInfo.onion),
child: Text(
serverInfo.onion,
overflow: TextOverflow.ellipsis,
),
);
}).toList()),
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).groupName),
CwtchLabel(label: AppLocalizations.of(context)!.groupName),
SizedBox(
height: 20,
),
CwtchTextField(controller: ctrlrGroupName, labelText: AppLocalizations.of(context).groupNameLabel, onChanged: (newValue) {}),
CwtchTextField(
controller: ctrlrGroupName,
labelText: AppLocalizations.of(context)!.groupNameLabel,
onChanged: (newValue) {},
validator: (value) {},
),
SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () {},
child: Text(AppLocalizations.of(context).createGroupBtn),
onPressed: () {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateGroup(profileOnion, server, ctrlrGroupName.text);
Future.delayed(const Duration(milliseconds: 500), () {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + " " + ctrlrGroupName.text));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context);
});
},
child: Text(AppLocalizations.of(context)!.createGroupBtn),
),
],
)));
}
/// TODO Join Group Pane
Widget joinGroupTab() {
return Container(
margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20),
child: Form(
autovalidateMode: AutovalidateMode.always,
key: _joinGroupFormKey,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context).joinGroupTab),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrContact,
validator: (value) {
if (value == "") {
return null;
}
if (globalErrorHandler.importBundleError) {
return AppLocalizations.of(context).invalidImportString;
} else if (globalErrorHandler.importBundleSuccess) {}
return null;
},
onChanged: (String importBundle) async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, importBundle);
Future.delayed(const Duration(milliseconds: 500), () {
if (globalErrorHandler.importBundleSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).successfullAddedContact + importBundle));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context);
}
});
},
)
])));
}
/// TODO Manage Servers Tab
Widget manageServersTab() {
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) {

View File

@ -16,7 +16,7 @@ import '../opaque.dart';
import '../settings.dart';
class AddEditProfileView extends StatefulWidget {
const AddEditProfileView({Key key}) : super(key: key);
const AddEditProfileView({Key? key}) : super(key: key);
@override
_AddEditProfileViewState createState() => _AddEditProfileViewState();
@ -30,8 +30,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
final ctrlrPass = TextEditingController(text: "");
final ctrlrPass2 = TextEditingController(text: "");
final ctrlrOnion = TextEditingController(text: "");
bool usePassword;
bool deleted;
late bool usePassword;
late bool deleted;
@override
void initState() {
@ -48,15 +48,15 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion;
return Scaffold(
appBar: AppBar(
title: Text(Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context).addProfileTitle : AppLocalizations.of(context).editProfileTitle),
title: Text(Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addProfileTitle : AppLocalizations.of(context)!.editProfileTitle),
),
body: _buildForm(),
);
}
void _handleSwitchPassword(bool value) {
void _handleSwitchPassword(bool? value) {
setState(() {
usePassword = value;
usePassword = value!;
});
}
@ -88,16 +88,18 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
diameter: 120,
maskOut: false,
border: theme.theme.portraitOnlineBorderColor(),
badgeTextColor: Colors.red,
badgeColor: Colors.red,
)
])),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context).displayNameLabel),
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrNick,
labelText: AppLocalizations.of(context).yourDisplayName,
labelText: AppLocalizations.of(context)!.yourDisplayName,
validator: (value) {
if (value.isEmpty) {
return "Please enter a display name";
@ -112,7 +114,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).addressLabel),
CwtchLabel(label: AppLocalizations.of(context)!.addressLabel),
SizedBox(
height: 20,
),
@ -120,29 +122,21 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
controller: ctrlrOnion,
onPressed: _copyOnion,
icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context).copyBtn,
tooltip: AppLocalizations.of(context)!.copyBtn,
)
])),
// We only allow setting password types on profile creation
Visibility(
visible: Provider.of<ProfileInfoState>(context).onion.isEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Radio(
value: false,
groupValue: usePassword,
Checkbox(
value: usePassword,
fillColor: MaterialStateProperty.all(theme.current().defaultButtonColor()),
activeColor: theme.current().defaultButtonActiveColor(),
onChanged: _handleSwitchPassword,
),
Text(
AppLocalizations.of(context).radioNoPassword,
style: TextStyle(color: theme.current().mainTextColor()),
),
Radio(
value: true,
groupValue: usePassword,
onChanged: _handleSwitchPassword,
),
Text(
AppLocalizations.of(context).radioUsePassword,
AppLocalizations.of(context)!.radioUsePassword,
style: TextStyle(color: theme.current().mainTextColor()),
),
])),
@ -155,7 +149,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context).currentPasswordLabel),
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
SizedBox(
height: 20,
),
@ -164,7 +158,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
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) {
return AppLocalizations.of(context).passwordErrorEmpty;
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
return null;
},
@ -173,7 +167,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
height: 20,
),
])),
CwtchLabel(label: AppLocalizations.of(context).password1Label),
CwtchLabel(label: AppLocalizations.of(context)!.password1Label),
SizedBox(
height: 20,
),
@ -182,10 +176,10 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
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) {
return AppLocalizations.of(context).passwordErrorEmpty;
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass2.value.text) {
return AppLocalizations.of(context).passwordErrorMatch;
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
},
@ -193,7 +187,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).password2Label),
CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
SizedBox(
height: 20,
),
@ -202,10 +196,10 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
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) {
return AppLocalizations.of(context).passwordErrorEmpty;
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass.value.text) {
return AppLocalizations.of(context).passwordErrorMatch;
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
}),
@ -214,10 +208,19 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox(
height: 20,
),
ElevatedButton(
onPressed: _createPressed,
style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()),
child: Text(Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context).addNewProfileBtn : AppLocalizations.of(context).saveProfileBtn),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
onPressed: _createPressed,
child: Text(
Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn,
textAlign: TextAlign.center,
),
),
),
],
),
Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
@ -226,7 +229,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
height: 20,
),
Tooltip(
message: AppLocalizations.of(context).enterCurrentPasswordForDelete,
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
child: ElevatedButton.icon(
onPressed: checkCurrentPassword()
? null
@ -235,7 +238,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
},
style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()),
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context).deleteBtn),
label: Text(AppLocalizations.of(context)!.deleteBtn),
))
]))
]))))));
@ -252,7 +255,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
// This will run all the validations in the form including
// checking that display name is not empty, and an actual check that the passwords
// match (and are provided if the user has requested an encrypted profile).
if (_formKey.currentState.validate()) {
if (_formKey.currentState!.validate()) {
if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty) {
if (usePassword == true) {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text);
@ -323,7 +326,7 @@ showAlertDialog(BuildContext context) {
foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context).deleteProfileConfirmBtn),
child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
onPressed: () {
// TODO Actually Delete the Peer
Navigator.of(context).pop(); // dismiss dialog
@ -332,7 +335,7 @@ showAlertDialog(BuildContext context) {
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context).deleteProfileConfirmBtn),
title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
actions: [
cancelButton,
continueButton,

View File

@ -12,14 +12,14 @@ import '../model.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ContactsView extends StatefulWidget {
const ContactsView({Key key}) : super(key: key);
const ContactsView({Key? key}) : super(key: key);
@override
_ContactsViewState createState() => _ContactsViewState();
}
class _ContactsViewState extends State<ContactsView> {
TextEditingController ctrlrFilter;
late TextEditingController ctrlrFilter;
bool showSearchBar = false;
@override
@ -31,67 +31,66 @@ class _ContactsViewState extends State<ContactsView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(children: [
ProfileImage(
imagePath: Provider.of<ProfileInfoState>(context).imagePath,
diameter: 42,
border: Provider.of<Settings>(context).theme.portraitOnlineBorderColor(),
),
SizedBox(
width: 10,
),
Expanded(
child: Text(
"%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname ?? Provider.of<ProfileInfoState>(context).onion ?? '').replaceAll("%2", "Contacts"),
overflow: TextOverflow.ellipsis,
)), //todo
]),
actions: [
IconButton(icon: TorIcon(), onPressed: _pushTorStatus),
IconButton(
icon: Icon(Icons.copy),
onPressed: _copyOnion,
),
IconButton(
// need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),
onPressed: () {
Provider.of<ContactListState>(context, listen: false).filter = "";
setState(() {
showSearchBar = !showSearchBar;
});
})
],
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddContact,
tooltip: AppLocalizations.of(context).tooltipAddContact,
child: const Icon(Icons.person_add_sharp),
),
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList(),
);
endDrawerEnableOpenDragGesture: false,
drawerEnableOpenDragGesture: false,
appBar: AppBar(
title: RepaintBoundary(
child: Row(children: [
ProfileImage(
imagePath: Provider.of<ProfileInfoState>(context).imagePath,
diameter: 42,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor(),
badgeTextColor: Colors.red,
badgeColor: Colors.red,
),
SizedBox(
width: 10,
),
Expanded(
child: Text("%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname).replaceAll("%2", "Contacts"),
overflow: TextOverflow.ellipsis, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor()))), //todo
])),
actions: [
IconButton(icon: TorIcon(), onPressed: _pushTorStatus),
IconButton(
// need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),
onPressed: () {
Provider.of<ContactListState>(context, listen: false).filter = "";
setState(() {
showSearchBar = !showSearchBar;
});
})
],
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddContact,
tooltip: AppLocalizations.of(context)!.tooltipAddContact,
child: const Icon(Icons.person_add_sharp),
),
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList());
}
Widget _buildFilterable() {
Widget txtfield = CwtchTextField(
controller: ctrlrFilter,
labelText: AppLocalizations.of(context).search,
onChanged: (newVal) {
Provider.of<ContactListState>(context, listen: false).filter = newVal;
});
controller: ctrlrFilter,
labelText: AppLocalizations.of(context)!.search,
onChanged: (newVal) {
Provider.of<ContactListState>(context, listen: false).filter = newVal;
},
);
return Column(children: [Padding(padding: EdgeInsets.all(8), child: txtfield), Expanded(child: _buildContactList())]);
}
Widget _buildContactList() {
final tiles = Provider.of<ContactListState>(context).filteredList().map((ContactInfoState contact) {
return ChangeNotifierProvider<ContactInfoState>.value(key: ValueKey(contact.profileOnion+""+contact.onion), value: contact, builder: (_, __) => ContactRow());
return ChangeNotifierProvider<ContactInfoState>.value(key: ValueKey(contact.profileOnion + "" + contact.onion), value: contact, builder: (_, __) => RepaintBoundary(child: ContactRow()));
});
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return ListView(children: divided);
return RepaintBoundary(child: ListView(children: divided));
}
void _pushAddContact() {
@ -117,10 +116,4 @@ class _ContactsViewState extends State<ContactsView> {
},
));
}
void _copyOnion() {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification)); //todo
// Find the Scaffold in the widget tree and use it to show a SnackBar.
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}

View File

@ -20,7 +20,9 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
children: <Widget>[
Flexible(
flex: flwtch.columns[0],
child: ContactsView(),
child: ContactsView(
key: widget.key,
),
),
Flexible(
flex: flwtch.columns[1],

View File

@ -24,7 +24,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).cwtchSettingsTitle),
title: Text(AppLocalizations.of(context)!.cwtchSettingsTitle),
),
body: _buildSettingsList(),
);
@ -43,13 +43,13 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
),
child: Column(children: [
ListTile(
title: Text(AppLocalizations.of(context).settingLanguage, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.settingLanguage, style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.language, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: Provider.of<Settings>(context).locale.languageCode,
onChanged: (String newValue) {
onChanged: (String? newValue) {
setState(() {
settings.switchLocale(Locale(newValue, ''));
settings.switchLocale(Locale(newValue!, ''));
saveSettings(context);
});
},
@ -60,7 +60,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
);
}).toList())),
SwitchListTile(
title: Text(AppLocalizations.of(context).settingTheme, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.settingTheme, style: TextStyle(color: settings.current().mainTextColor())),
value: settings.current() == Opaque.light,
onChanged: (bool value) {
if (value) {
@ -75,11 +75,11 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
secondary: Icon(Icons.lightbulb_outline, 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", style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: "Single",
onChanged: (String newValue) {
onChanged: (String? newValue) {
if (newValue == "Double (1:2)") {
Provider.of<FlwtchState>(context).columns = [1, 2];
} else if (newValue == "Double (1:4)") {
@ -95,8 +95,8 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
);
}).toList())),
SwitchListTile(
title: Text(AppLocalizations.of(context).blockUnknownLabel, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context).descriptionBlockUnknownConnections),
title: Text(AppLocalizations.of(context)!.blockUnknownLabel, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.descriptionBlockUnknownConnections),
value: settings.blockUnknownConnections,
onChanged: (bool value) {
if (value) {
@ -111,8 +111,8 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
secondary: Icon(Icons.app_blocking, color: settings.current().mainTextColor()),
),
SwitchListTile(
title: Text(AppLocalizations.of(context).experimentsEnabled, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context).descriptionExperiments),
title: Text(AppLocalizations.of(context)!.experimentsEnabled, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.descriptionExperiments),
value: settings.experimentsEnabled,
onChanged: (bool value) {
if (value) {
@ -130,9 +130,9 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
child: Column(
children: [
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],
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]!,
onChanged: (bool value) {
if (value) {
settings.enableExperiment(TapirGroupsExperiment);
@ -156,7 +156,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
height: 128,
)),
applicationName: "Cwtch (Flutter UI)",
applicationVersion: AppLocalizations.of(context).version.replaceAll("%1", constructVersionString(Provider.of<Settings>(context).packageInfo)),
applicationVersion: AppLocalizations.of(context)!.version.replaceAll("%1", constructVersionString(Provider.of<Settings>(context).packageInfo)),
applicationLegalese: '\u{a9} 2021 Open Privacy Research Society',
),
]))));
@ -177,22 +177,22 @@ String constructVersionString(PackageInfo pinfo) {
/// an individual language code. There might be a more efficient way of doing this.
String getLanguageFull(context, String languageCode) {
if (languageCode == "en") {
return AppLocalizations.of(context).localeEn;
return AppLocalizations.of(context)!.localeEn;
}
if (languageCode == "es") {
return AppLocalizations.of(context).localeEs;
return AppLocalizations.of(context)!.localeEs;
}
if (languageCode == "fr") {
return AppLocalizations.of(context).localeFr;
return AppLocalizations.of(context)!.localeFr;
}
if (languageCode == "pt") {
return AppLocalizations.of(context).localePt;
return AppLocalizations.of(context)!.localePt;
}
if (languageCode == "de") {
return AppLocalizations.of(context).localeDe;
return AppLocalizations.of(context)!.localeDe;
}
if (languageCode == "it") {
return AppLocalizations.of(context).localeIt;
return AppLocalizations.of(context)!.localeIt;
}
return languageCode;
}

View File

@ -42,7 +42,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(Provider.of<ContactInfoState>(context).nickname + " " + AppLocalizations.of(context).conversationSettings),
title: Text(Provider.of<ContactInfoState>(context).nickname + " " + AppLocalizations.of(context)!.conversationSettings),
),
body: _buildSettingsList(),
);
@ -60,40 +60,15 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
minHeight: viewportConstraints.maxHeight,
),
child: Container(
margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20),
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(2),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// Address Copy Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).groupAddr),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrGroupAddr,
)
]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).server),
SizedBox(
height: 20,
),
CwtchTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).server),
)
]),
// Nickname Save Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).displayNameLabel),
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
SizedBox(
height: 20,
),
@ -105,21 +80,70 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetGroupAttribute(profileOnion, handle, "local.name", ctrlrNick.text);
// todo translations
final snackBar = SnackBar(content: Text("Group Nickname changed successfully"));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
icon: Icon(Icons.save),
tooltip: AppLocalizations.of(context).saveBtn,
tooltip: AppLocalizations.of(context)!.saveBtn,
)
]),
// Address Copy Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.groupAddr),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrGroupAddr,
labelText: '',
validator: (value) {},
)
]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).conversationSettings),
CwtchLabel(label: AppLocalizations.of(context)!.server),
SizedBox(
height: 20,
),
CwtchTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).server),
validator: (value) {},
labelText: '',
)
]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.conversationSettings),
SizedBox(
height: 20,
),
// TODO
]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.rejectGroupBtn,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(Icons.delete),
label: Text(AppLocalizations.of(context)!.deleteBtn),
))
])
])))));
});
});
@ -127,7 +151,47 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ContactInfoState>(context, listen: false).onion));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification));
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)!.deleteProfileConfirmBtn),
onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.LeaveGroup(profileOnion, handle);
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)!.deleteProfileConfirmBtn),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/views/peersettingsview.dart';
import 'package:cwtch/widgets/DropdownContacts.dart';
@ -86,7 +87,7 @@ class _MessageViewState extends State<MessageView> {
));
}
void _sendMessage([String ignoredParam]) {
void _sendMessage([String? ignoredParam]) {
ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
@ -94,7 +95,7 @@ class _MessageViewState extends State<MessageView> {
_sendMessageHelper();
}
void _sendInvitation([String ignoredParam]) {
void _sendInvitation([String? ignoredParam]) {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, this.selectedContact);
@ -114,37 +115,36 @@ class _MessageViewState extends State<MessageView> {
Widget _buildComposeBox() {
return Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor(),
padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2),
height: 100,
padding: EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Expanded(
child: TextField(
key: Key('txtCompose'),
controller: ctrlrCompose,
focusNode: focusNode,
textInputAction: TextInputAction.send,
onSubmitted: _sendMessage,
)),
Column(children: [
SizedBox(
width: 100,
height: 50,
child: Padding(
padding: EdgeInsets.fromLTRB(2, 2, 2, 2),
child: ElevatedButton(
child: Icon(Icons.send, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
style: ButtonStyle(
fixedSize: MaterialStateProperty.all(Size(86, 50)),
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.defaultButtonColor()),
child: Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor()))),
child: TextFormField(
key: Key('txtCompose'),
controller: ctrlrCompose,
autofocus: true,
focusNode: focusNode,
textInputAction: TextInputAction.send,
onFieldSubmitted: _sendMessage,
decoration: InputDecoration(
enabledBorder: InputBorder.none,
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",
onPressed: () => _modalSendInvitation(context)),
suffixIcon: IconButton(
icon: Icon(Icons.send, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: "Send Message",
onPressed: _sendMessage,
),
onPressed: _sendMessage,
))),
SizedBox(
width: 86,
height: 40,
child: IconButton(icon: Icon(Icons.insert_invitation, size: 12, color: Provider.of<Settings>(context).theme.mainTextColor()), onPressed: () => _modalSendInvitation(context))),
])
),
],
),
);
@ -168,7 +168,7 @@ class _MessageViewState extends State<MessageView> {
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(bcontext).invitationLabel),
Text(AppLocalizations.of(bcontext)!.invitationLabel),
SizedBox(
height: 20,
),
@ -183,7 +183,7 @@ class _MessageViewState extends State<MessageView> {
height: 20,
),
ElevatedButton(
child: Text(AppLocalizations.of(bcontext).inviteBtn, semanticsLabel: AppLocalizations.of(bcontext).inviteBtn),
child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn),
onPressed: () {
this._sendInvitation();
Navigator.pop(bcontext);

View File

@ -55,31 +55,11 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
minHeight: viewportConstraints.maxHeight,
),
child: Container(
margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// Address Copy Button
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(2),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).addressLabel),
SizedBox(
height: 20,
),
CwtchButtonTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion),
onPressed: _copyOnion,
icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context).copyBtn,
)
]),
// Nickname Save Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).displayNameLabel),
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
SizedBox(
height: 20,
),
@ -96,21 +76,41 @@ 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"));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
icon: Icon(Icons.save),
tooltip: AppLocalizations.of(context).saveBtn,
tooltip: AppLocalizations.of(context)!.saveBtn,
)
]),
// Address Copy Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.addressLabel),
SizedBox(
height: 20,
),
CwtchButtonTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion),
onPressed: _copyOnion,
icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context)!.copyBtn,
)
]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).conversationSettings),
CwtchLabel(label: AppLocalizations.of(context)!.conversationSettings),
SizedBox(
height: 20,
),
SwitchListTile(
title: Text(AppLocalizations.of(context).blockBtn, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.blockBtn, style: TextStyle(color: settings.current().mainTextColor())),
value: Provider.of<ContactInfoState>(context).isBlocked,
onChanged: (bool blocked) {
// Save local blocked status
@ -141,15 +141,15 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
secondary: Icon(Icons.block, color: settings.current().mainTextColor()),
),
ListTile(
title: Text(AppLocalizations.of(context).savePeerHistory, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context).savePeerHistoryDescription),
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()),
trailing: DropdownButton(
value: Provider.of<ContactInfoState>(context).savePeerHistory == "DefaultDeleteHistory"
? AppLocalizations.of(context).dontSavePeerHistory
? AppLocalizations.of(context)!.dontSavePeerHistory
: (Provider.of<ContactInfoState>(context).savePeerHistory == "SaveHistory"
? AppLocalizations.of(context).savePeerHistory
: AppLocalizations.of(context).dontSavePeerHistory),
? AppLocalizations.of(context)!.savePeerHistory
: AppLocalizations.of(context)!.dontSavePeerHistory),
onChanged: (newValue) {
setState(() {
// Set whether or not to dave the Contact History...
@ -157,7 +157,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
var onion = Provider.of<ContactInfoState>(context, listen: false).onion;
const SaveHistoryKey = "SavePeerHistory";
if (newValue == AppLocalizations.of(context).savePeerHistory) {
if (newValue == AppLocalizations.of(context)!.savePeerHistory) {
Provider.of<ContactInfoState>(context, listen: false).savePeerHistory = "SaveHistory";
final setPeerAttribute = {
"EventType": "SetPeerAttribute",
@ -177,7 +177,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
}
});
},
items: [AppLocalizations.of(context).savePeerHistory, AppLocalizations.of(context).dontSavePeerHistory].map<DropdownMenuItem<String>>((String value) {
items: [AppLocalizations.of(context)!.savePeerHistory, AppLocalizations.of(context)!.dontSavePeerHistory].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
@ -191,7 +191,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ContactInfoState>(context, listen: false).onion));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}

View File

@ -10,6 +10,7 @@ import 'package:cwtch/widgets/profilerow.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../model.dart';
import '../opaque.dart';
import 'addeditprofileview.dart';
import 'globalsettingsview.dart';
@ -31,36 +32,52 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
@override
Widget build(BuildContext context) {
// 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?
return new WillPopScope(
onWillPop: () async => false,
child: Scaffold(
backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor(),
appBar: AppBar(
title: Text(AppLocalizations.of(context).titleManageProfiles),
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,
return Consumer<Settings>(
// Prevents Android back button from closing the app on the profile manager screen
// (which would shutdown connections and all kinds of other expensive to generate things)
// TODO pop up a dialogue regarding closing the app?
builder: (context, settings, child) => WillPopScope(
onWillPop: () async => 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(),
),
SizedBox(
width: 10,
),
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),
],
),
),
body: _buildProfileManager(), //_buildSuggestions(),
));
floatingActionButton: FloatingActionButton(
onPressed: _pushAddEditProfile,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
),
),
body: _buildProfileManager(),
)),
);
}
void _setLoggingLevelDebug() {
@ -112,55 +129,83 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
void _modalUnlockProfiles() {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(context).enterProfilePassword),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPassword,
),
SizedBox(
height: 20,
),
ElevatedButton(
child: Text(AppLocalizations.of(context).unlock, semanticsLabel: AppLocalizations.of(context).unlock),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.LoadProfiles(ctrlrPassword.value.text);
ctrlrPassword.text = "";
Navigator.pop(context);
},
),
],
)),
));
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(context)!.enterProfilePassword),
SizedBox(
height: 20,
),
CwtchPasswordField(
autofocus: true,
controller: ctrlrPassword,
action: unlock,
validator: (value) {},
),
SizedBox(
height: 20,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock),
onPressed: () {
unlock(ctrlrPassword.value.text);
},
)),
Spacer()
]),
],
))),
)));
});
}
void unlock(String password) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LoadProfiles(password);
ctrlrPassword.text = "";
Navigator.pop(context);
}
Widget _buildProfileManager() {
final tiles = Provider.of<ProfileListState>(context).profiles.map(
(ProfileInfoState profile) {
return ChangeNotifierProvider<ProfileInfoState>.value(
value: profile,
builder: (context, child) => ProfileRow(),
return Consumer<ProfileListState>(
builder: (context, pls, child) {
final tiles = pls.profiles.map(
(ProfileInfoState profile) {
return ChangeNotifierProvider<ProfileInfoState>.value(
value: profile,
builder: (context, child) => RepaintBoundary(child: ProfileRow()),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
if (tiles.isEmpty) {
return const Center(
child: const Text(
"Please create or unlock a profile to begin!",
textAlign: TextAlign.center,
));
}
return ListView(children: divided);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return ListView(children: divided);
}
}

View File

@ -4,10 +4,8 @@ import 'package:flutter/material.dart';
class SplashView extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("SplashView build()");
return Scaffold(
appBar: AppBar(title: Text("Cwtch")),
body: Center(child: Column(children: <Widget>[Text("Loading Cwtch...")])),
return const Scaffold(
body: const Center(child: const Text("Loading Cwtch...")),
);
}
}

View File

@ -44,7 +44,7 @@ class _TorStatusView extends State<TorStatusView> {
ListTile(
leading: TorIcon(),
title: Text("Tor Status"),
subtitle: Text(torStatus.progress == 100 ? AppLocalizations.of(context).networkStatusOnline : torStatus.status),
subtitle: Text(torStatus.progress == 100 ? AppLocalizations.of(context)!.networkStatusOnline : torStatus.status),
trailing: ElevatedButton(
child: Text("Reset"),
onPressed: () {

View File

@ -9,7 +9,7 @@ import '../model.dart';
// Pass an onChanged handler to access value
class DropdownContacts extends StatefulWidget {
DropdownContacts({
this.onChanged,
required this.onChanged,
});
final Function(dynamic) onChanged;
@ -18,22 +18,20 @@ class DropdownContacts extends StatefulWidget {
}
class _DropdownContactsState extends State<DropdownContacts> {
String selected;
String? selected;
@override
Widget build(BuildContext context) {
return DropdownButton(
value: this.selected,
items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.map<DropdownMenuItem<String>>((ContactInfoState contact) {
return DropdownMenuItem<String>(value: contact.onion, child: Text(contact.nickname ?? contact.onion));
return DropdownMenuItem<String>(value: contact.onion, child: Text(contact.nickname));
}).toList(),
onChanged: (newVal) {
onChanged: (String? newVal) {
setState(() {
this.selected = newVal;
});
if (widget.onChanged != null) {
widget.onChanged(newVal);
}
widget.onChanged(newVal);
});
}
}

View File

@ -5,9 +5,9 @@ import 'package:provider/provider.dart';
// Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator.
class CwtchButtonTextField extends StatefulWidget {
CwtchButtonTextField({this.controller, this.onPressed, this.icon, this.tooltip, this.readonly = true});
CwtchButtonTextField({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true});
final TextEditingController controller;
final Function onPressed;
final Function()? onPressed;
final Icon icon;
final String tooltip;
final bool readonly;
@ -37,6 +37,7 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
),
floatingLabelBehavior: FloatingLabelBehavior.never,
filled: true,
fillColor: theme.current().textfieldBackgroundColor(),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
@ -44,10 +45,8 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
color: theme.current().textfieldErrorColor(),
fontWeight: FontWeight.bold,
),
fillColor: theme.current().textfieldBackgroundColor(),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
);
});
}

View File

@ -87,18 +87,19 @@ class _ContactRowState extends State<ContactRow> {
}
void _pushMessageView(String handle) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle).unreadMessages = 0;
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>(
builder: (BuildContext builderContext) {
var profile = Provider.of<FlwtchState>(builderContext, listen: false).profs.getProfile(profileOnion);
// assert we have an actual profile...
var profile = Provider.of<FlwtchState>(builderContext, listen: false).profs.getProfile(profileOnion)!;
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
],
builder:(context, child) => MessageView(),
builder: (context, child) => MessageView(),
);
},
),
@ -123,8 +124,13 @@ class _ContactRowState extends State<ContactRow> {
String dateToNiceString(DateTime date) {
if (date.millisecondsSinceEpoch == 0) {
return AppLocalizations.of(context).dateNever;
return AppLocalizations.of(context)!.dateNever;
}
return DateFormat.yMd().add_jm().format(date.toLocal());
// If the last message was over a day ago, just state the date
if (DateTime.now().difference(date).inDays > 1) {
return DateFormat.yMd().format(date.toLocal());
}
// Otherwise just state the time.
return DateFormat.Hms().format(date.toLocal());
}
}

View File

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

View File

@ -37,11 +37,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
var senderDisplayStr = "";
if (Provider.of<MessageState>(context).senderOnion != null) {
var contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (contact == null) {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
} else {
senderDisplayStr = contact.nickname ?? contact.onion;
}
senderDisplayStr = contact!.nickname;
}
var wdgSender = Center(
widthFactor: 1,

View File

@ -22,16 +22,18 @@ class MessageBubbleState extends State<MessageBubble> {
if (Provider.of<MessageState>(context).timestamp != null) {
// user-configurable timestamps prolly ideal? #todo
prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp);
DateTime messageDate = Provider.of<MessageState>(context).timestamp;
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
}
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (Provider.of<MessageState>(context).senderOnion != null) {
var contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (contact == null) {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
if (!fromMe && Provider.of<MessageState>(context).senderOnion != null) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = contact.nickname ?? contact.onion;
senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
}
}
var wdgSender = SelectableText(senderDisplayStr,
@ -71,26 +73,27 @@ class MessageBubbleState extends State<MessageBubble> {
return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString());
return Container(
return RepaintBoundary(
child: Container(
decoration: BoxDecoration(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(),
border:
Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(), width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
),
),
child: Padding(
padding: EdgeInsets.all(9.0),
child: Column(
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))));
child: Container(
decoration: BoxDecoration(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(),
border: Border.all(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(), width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
),
),
child: Padding(
padding: EdgeInsets.all(9.0),
child: Column(
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
});
}
}

View File

@ -15,41 +15,42 @@ class _MessageListState extends State<MessageList> {
@override
Widget build(BuildContext outerContext) {
return Card(
child: Scrollbar(
isAlwaysShown: true,
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,
image: AssetImage("assets/core/negative_heart_512px.png"),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.mainTextColor(), BlendMode.srcIn))),
child: ListView.builder(
return RepaintBoundary(
child: Container(
child: Scrollbar(
isAlwaysShown: true,
controller: ctrlr1,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true,
itemBuilder: (itemBuilderContext, index) {
var trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
return ChangeNotifierProvider(
key: ValueKey(trueIndex),
create: (x) => MessageState(
context: itemBuilderContext,
profileOnion: Provider.of<ProfileInfoState>(outerContext, listen: false).onion,
contactHandle: Provider.of<ContactInfoState>(x, listen: false).onion,
messageIndex: trueIndex,
),
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(outerContext).isGroup == true && Provider.of<MessageState>(bcontext).signature.isEmpty == false
? Provider.of<MessageState>(bcontext).signature
: trueIndex.toString();
return MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
});
},
),
)));
child: Container(
// Only show broken heart is the contact is offline...
decoration: BoxDecoration(
image: Provider.of<ContactInfoState>(outerContext).isOnline()
? null
: DecorationImage(
fit: BoxFit.contain,
image: AssetImage("assets/core/negative_heart_512px.png"),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.mainTextColor(), BlendMode.srcIn))),
child: ListView.builder(
controller: ctrlr1,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true,
itemBuilder: (itemBuilderContext, index) {
var trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
return ChangeNotifierProvider(
key: ValueKey(trueIndex),
create: (x) => MessageState(
context: itemBuilderContext,
profileOnion: Provider.of<ProfileInfoState>(outerContext, listen: false).onion,
contactHandle: Provider.of<ContactInfoState>(x, listen: false).onion,
messageIndex: trueIndex,
),
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(outerContext).isGroup == true && Provider.of<MessageState>(bcontext).signature.isEmpty == false
? Provider.of<MessageState>(bcontext).signature
: trueIndex.toString();
return RepaintBoundary(child: MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)));
});
},
),
))));
}
}

View File

@ -14,7 +14,7 @@ import 'messagebubble.dart';
import 'messageloadingbubble.dart';
class MessageRow extends StatefulWidget {
MessageRow({Key key}) : super(key: key);
MessageRow({Key? key}) : super(key: key);
@override
_MessageRowState createState() => _MessageRowState();
@ -52,10 +52,12 @@ class _MessageRowState extends State<MessageRow> {
child: Padding(
padding: EdgeInsets.all(4.0),
child: ProfileImage(
diameter: 48.0,
imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor())));
diameter: 48.0,
imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
badgeTextColor: Colors.red, badgeColor: Colors.red,
)));
widgetRow = <Widget>[
wdgPortrait,
@ -76,7 +78,7 @@ class _MessageRowState extends State<MessageRow> {
case 101:
return InvitationBubble();
}
return null;
return MalformedBubble();
}
void _btnAdd() {
@ -94,7 +96,7 @@ 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));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}

View File

@ -5,25 +5,49 @@ import '../settings.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({this.controller, this.validator});
CwtchPasswordField({required this.controller, required this.validator, this.action, this.autofocus = true});
final TextEditingController controller;
final FormFieldValidator validator;
final Function(String)? action;
final bool autofocus;
@override
_CwtchTextFieldState createState() => _CwtchTextFieldState();
}
class _CwtchTextFieldState extends State<CwtchPasswordField> {
bool obscureText = true;
@override
Widget build(BuildContext context) {
// todo: translations
var label = "View Password";
if (!obscureText) {
label = "Hide Password";
}
return Consumer<Settings>(builder: (context, theme, child) {
return TextFormField(
autofocus: widget.autofocus,
controller: widget.controller,
validator: widget.validator,
obscureText: true,
obscureText: obscureText,
onFieldSubmitted: widget.action,
textInputAction: TextInputAction.unspecified,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () {
obscureText = !obscureText;
},
icon: Icon((obscureText ? Icons.remove_red_eye : Icons.remove_red_eye_outlined), semanticLabel: label),
tooltip: label,
color: theme.current().mainTextColor(),
highlightColor: theme.current().defaultButtonColor(),
focusColor: theme.current().defaultButtonActiveColor(),
splashColor: theme.current().defaultButtonActiveColor(),
),
errorStyle: TextStyle(
color: theme.current().textfieldErrorColor(),
fontWeight: FontWeight.bold,
@ -35,7 +59,6 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
fillColor: theme.current().textfieldBackgroundColor(),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
);
});
}

View File

@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
import '../settings.dart';
class ProfileImage extends StatefulWidget {
ProfileImage({this.imagePath, this.diameter, this.border, this.badgeCount = 0, this.badgeColor, this.badgeTextColor, this.maskOut = false});
ProfileImage({required this.imagePath, required this.diameter, required this.border, this.badgeCount = 0, required this.badgeColor, required this.badgeTextColor, this.maskOut = false});
final double diameter;
final String imagePath;
final Color border;
@ -21,7 +21,8 @@ class ProfileImage extends StatefulWidget {
class _ProfileImageState extends State<ProfileImage> {
@override
Widget build(BuildContext context) {
return Stack(children: [
return RepaintBoundary(
child: Stack(children: [
ClipOval(
clipBehavior: Clip.antiAlias,
child: Container(
@ -57,6 +58,6 @@ class _ProfileImageState extends State<ProfileImage> {
child: Text(widget.badgeCount > 99 ? "99+" : widget.badgeCount.toString(), style: TextStyle(color: widget.badgeTextColor, fontSize: 8.0)),
),
)),
]);
]));
}
}

View File

@ -55,7 +55,7 @@ class _ProfileRowState extends State<ProfileRow> {
)),
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context).editProfile + " " + profile.nickname,
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);
@ -88,6 +88,7 @@ class _ProfileRowState extends State<ProfileRow> {
void _pushContactList(ProfileInfoState profile, bool includeDoublePane) {
Navigator.of(context).push(
MaterialPageRoute<void>(
settings: RouteSettings(name: "conversations"),
builder: (BuildContext buildcontext) {
return MultiProvider(
providers: [

View File

@ -7,10 +7,10 @@ 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({this.controller, this.labelText, this.validator, this.onChanged = doNothing});
CwtchTextField({required this.controller, required this.labelText, this.validator = null, this.onChanged = doNothing});
final TextEditingController controller;
final String labelText;
final FormFieldValidator validator;
final FormFieldValidator? validator;
final Function(String) onChanged;
@override
@ -40,7 +40,6 @@ class _CwtchTextFieldState extends State<CwtchTextField> {
fillColor: theme.current().textfieldBackgroundColor(),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
);
});
}

View File

@ -15,7 +15,8 @@ class TorIcon extends StatefulWidget {
class _TorIconState extends State<TorIcon> {
@override
Widget build(BuildContext context) {
return Image(
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")),
@ -23,8 +24,8 @@ class _TorIconState extends State<TorIcon> {
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),
);
? AppLocalizations.of(context)!.networkStatusOnline
: (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor),
));
}
}

View File

@ -18,7 +18,7 @@ class _TorStatusState extends State<TorStatusLabel> {
stream: Provider.of<FlwtchState>(context).appStatus.torStatus(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return Text(
snapshot.hasData ? snapshot.data : AppLocalizations.of(context).loadingTor,
snapshot.hasData ? snapshot.data! : AppLocalizations.of(context)!.loadingTor,
style: Theme.of(context).textTheme.headline4,
);
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -4,6 +4,10 @@
#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

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

View File

@ -8,6 +8,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.2"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
async:
dependency: transitive
description:
@ -63,7 +70,21 @@ packages:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.3"
dbus:
dependency: transitive
description:
name: dbus
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
desktop_notifications:
dependency: "direct main"
description:
name: desktop_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
fake_async:
dependency: transitive
description:
@ -84,7 +105,7 @@ packages:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.0"
version: "6.1.1"
flutter:
dependency: "direct main"
description: flutter
@ -128,7 +149,7 @@ packages:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.1"
version: "0.13.3"
http_parser:
dependency: transitive
description:
@ -184,42 +205,42 @@ packages:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.1"
package_info_plus_linux:
dependency: transitive
description:
name: package_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.2"
package_info_plus_macos:
dependency: transitive
description:
name: package_info_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.1.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.1"
package_info_plus_web:
dependency: transitive
description:
name: package_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.1"
package_info_plus_windows:
dependency: transitive
description:
name: package_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.0.1"
path:
dependency: transitive
description:
@ -261,7 +282,7 @@ packages:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.1"
pedantic:
dependency: transitive
description:
@ -269,6 +290,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.0"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
platform:
dependency: transitive
description:
@ -296,7 +324,7 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2+3"
version: "5.0.0"
sky_engine:
dependency: transitive
description: flutter
@ -385,7 +413,16 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
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:
@ -393,6 +430,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
sdks:
dart: ">=2.12.0 <3.0.0"
dart: ">=2.13.0 <3.0.0"
flutter: ">=1.20.0"

View File

@ -18,12 +18,12 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
provider: "4.3.2+3"
provider: 5.0.0
package_info_plus: ^1.0.0
#intl_translation: any
flutter_localizations:
@ -34,6 +34,7 @@ dependencies:
cupertino_icons: ^1.0.0
ffi: ^1.0.0
path_provider: ^2.0.0
desktop_notifications: 0.5.0
glob: any
# todo: flutter_driver causes version conflict. eg https://github.com/flutter/flutter/issues/44829
@ -44,6 +45,12 @@ dependencies:
flutter_driver:
sdk: flutter
window_size:
git:
url: git://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81
#dev_dependencies:
# flutter_lokalise: any

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 219 B

View File

@ -39,7 +39,7 @@ void main() {
home: Card(child: CwtchButtonTextField(
icon: Icon(Icons.bug_report_outlined),
tooltip: testingStr,
controller: ctrlr1,
controller: ctrlr1, onPressed: () { },
)),
);}
));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -27,7 +27,7 @@ void main() {
tester.binding.window.physicalSizeTestValue = Size(800, 300);
final TextEditingController ctrlr1 = TextEditingController();
Widget testWidget = CwtchTextField(controller: ctrlr1);
Widget testWidget = CwtchTextField(controller: ctrlr1, validator: (value) { }, labelText: '',);
Widget testHarness = MultiProvider(
providers:[getSettingsEnglishDark()],
@ -76,7 +76,7 @@ void main() {
if (number == null) return strFail2;
return null;
},
onChanged: (value) => formKey.currentState.validate(),
onChanged: (value) => formKey.currentState!.validate(),
);
Widget testHarness = MultiProvider(
@ -125,7 +125,7 @@ void main() {
await tester.pumpAndSettle();
// empty string
formKey.currentState.validate(); //(ctrlr1.clear() doesn't trigger validate like keypress does)
formKey.currentState!.validate(); //(ctrlr1.clear() doesn't trigger validate like keypress does)
await tester.pumpAndSettle();
await expectLater(find.byWidget(testHarness), matchesGoldenFile(file('form_final')));
expect(find.text(strFail1), findsOneWidget);