Merge branch 'trunk' of git.openprivacy.ca:flutter/flutter_app into androidservice

This commit is contained in:
erinn 2021-05-28 15:56:54 -07:00
commit dfe5f500c6
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: and then use:
``` ```
Text(AppLocalizations.of(context).stringIdentifer), Text(AppLocalizations.of(context)!.stringIdentifer),
``` ```
### Configuration ### 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 - [ ] Leave Group
- [ ] Pressing Back should go back to the message pane for the 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 <application
android:name="io.flutter.app.FlutterApplication" android:name="io.flutter.app.FlutterApplication"
android:label="Cwtch" android:label="Cwtch"
android:extractNativeLibs="true"
android:icon="@mipmap/knott"> android:icon="@mipmap/knott">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@ -185,6 +185,17 @@ class MainActivity: FlutterActivity() {
val value = (call.argument("value") as? String) ?: ""; val value = (call.argument("value") as? String) ?: "";
Cwtch.setGroupAttribute(profile, groupHandle, key, value); 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" -> { "RejectInvite" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: ""; val profile = (call.argument("ProfileOnion") as? String) ?: "";
val groupHandle = (call.argument("groupHandle") 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); void DebugResetContact(String profileOnion, String contactHandle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> ACNEvents(); Future<dynamic> ACNEvents();
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> ContactEvents(); Future<dynamic> ContactEvents();
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> GetProfiles(); Future<dynamic> GetProfiles();
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<int> NumMessages(String profile, String handle); Future<dynamic> NumMessages(String profile, String handle);
// ignore: non_constant_identifier_names // 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 // 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 // ignore: non_constant_identifier_names
void SendMessage(String profile, String handle, String message); void SendMessage(String profile, String handle, String message);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendInvitation(String profile, String handle, String target); 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 // ignore: non_constant_identifier_names
void ImportBundle(String profile, String bundle); void ImportBundle(String profile, String bundle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/notification_manager.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:cwtch/torstatus.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) // 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 // Takes Notifiers and triggers them on appropriate events
class CwtchNotifier { class CwtchNotifier {
ProfileListState profileCN; late ProfileListState profileCN;
Settings settings; late Settings settings;
ErrorHandler error; late ErrorHandler error;
TorStatus torStatus; 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; profileCN = pcn;
settings = settingsCN; settings = settingsCN;
error = errorCN; error = errorCN;
torStatus = torStatusCN; torStatus = torStatusCN;
notificationManager = notificationManagerP;
} }
void handleMessage(String type, dynamic data) { 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")); onion: data["Identity"], nickname: data["name"], imagePath: data["picture"], contactsJson: data["ContactsJson"], serversJson: data["ServerList"], online: data["Online"] == "true"));
break; break;
case "PeerCreated": case "PeerCreated":
profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState( profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(
data["ProfileOnion"], data["ProfileOnion"],
data["RemotePeer"], data["RemotePeer"],
nickname: data["nick"], 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 lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
)); ));
break; 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": 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 (contact != null) {
if (data["ConnectionState"] != null) { if (data["ConnectionState"] != null) {
contact.status = data["ConnectionState"]; contact.status = data["ConnectionState"];
@ -56,13 +69,14 @@ class CwtchNotifier {
contact.isBlocked = data["authorization"] == "blocked"; contact.isBlocked = data["authorization"] == "blocked";
} }
// contact.[status/isBlocked] might change the list's sort order // contact.[status/isBlocked] might change the list's sort order
profileCN.getProfile(data["ProfileOnion"]).contactList.resort(); profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
} }
break; break;
case "NewMessageFromPeer": case "NewMessageFromPeer":
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).unreadMessages++; notificationManager.notify("New Message From Peer!");
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).totalMessages++; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++;
profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now()); profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
break; break;
case "PeerAcknowledgement": case "PeerAcknowledgement":
// We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end... // 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"]; var idx = data["Index"];
// We return -1 for protocol message acks if there is no message // We return -1 for protocol message acks if there is no message
if (idx == "-1") break; 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; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext, listen: false); var message = Provider.of<MessageState>(key.currentContext!, listen: false);
if (message == null) break; if (message == null) break;
message.ackd = true; message.ackd = true;
} catch (e) { } catch (e) {
@ -85,16 +99,16 @@ class CwtchNotifier {
case "NewMessageFromGroup": case "NewMessageFromGroup":
if (data["ProfileOnion"] != data["RemotePeer"]) { if (data["ProfileOnion"] != data["RemotePeer"]) {
//not from me //not from me
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).unreadMessages++; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).totalMessages++; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["GroupID"], DateTime.now()); profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
} else { } else {
// from me (already displayed - do not update counter) // from me (already displayed - do not update counter)
var idx = data["Signature"]; 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; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext, listen: false); var message = Provider.of<MessageState>(key.currentContext!, listen: false);
if (message == null) break; if (message == null) break;
message.ackd = true; message.ackd = true;
} catch (e) { } catch (e) {
@ -102,14 +116,25 @@ class CwtchNotifier {
} }
} }
break; 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": case "SendMessageToGroupError":
// from me (already displayed - do not update counter) // from me (already displayed - do not update counter)
print("SendMessageToGroupError: $data"); print("SendMessageToGroupError: $data");
var idx = data["Signature"]; 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; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext, listen: false); var message = Provider.of<MessageState>(key.currentContext!, listen: false);
if (message == null) break; if (message == null) break;
message.error = true; message.error = true;
} catch (e) { } catch (e) {
@ -118,28 +143,30 @@ class CwtchNotifier {
break; break;
case "AppError": case "AppError":
print("New App Error: $data"); print("New App Error: $data");
error.handleUpdate(data["Data"]); if (data["Data"] != null) {
error.handleUpdate(data["Data"]);
}
break; break;
case "UpdateGlobalSettings": case "UpdateGlobalSettings":
settings.handleUpdate(jsonDecode(data["Data"])); settings.handleUpdate(jsonDecode(data["Data"]));
break; break;
case "SetAttribute": case "SetAttribute":
if (data["Key"] == "public.name") { if (data["Key"] == "public.name") {
profileCN.getProfile(data["ProfileOnion"]).nickname = data["Data"]; profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"];
} else { } else {
print("unhandled set attribute event: $type"); print("unhandled set attribute event: $type $data");
} }
break; break;
case "NetworkError": case "NetworkError":
var isOnline = data["Status"] == "Success"; var isOnline = data["Status"] == "Success";
profileCN.getProfile(data["ProfileOnion"]).isOnline = isOnline; profileCN.getProfile(data["ProfileOnion"])?.isOnline = isOnline;
break; break;
case "ACNStatus": case "ACNStatus":
print("acn status: $data"); print("acn status: $data");
torStatus.handleUpdate(int.parse(data["Progress"]), data["Status"]); torStatus.handleUpdate(int.parse(data["Progress"]), data["Status"]);
break; break;
case "UpdateServerInfo": case "UpdateServerInfo":
profileCN.getProfile(data["ProfileOnion"]).replaceServers(data["ServerList"]); profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]);
break; break;
case "NewGroup": case "NewGroup":
print("new group invite: $data"); print("new group invite: $data");
@ -148,30 +175,38 @@ class CwtchNotifier {
String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5))); String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5)));
dynamic groupInvite = jsonDecode(inviteJson); dynamic groupInvite = jsonDecode(inviteJson);
print("new group invite: $groupInvite"); print("new group invite: $groupInvite");
if (profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(groupInvite["GroupID"]) == null) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"], 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())); 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; break;
case "AcceptGroupInvite": case "AcceptGroupInvite":
print("accept group invite: $data"); print("accept group invite: $data");
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).isInvitation = false; 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.updateLastMessageTime(data["GroupID"], DateTime.now());
break; break;
case "ServerStateChange": case "ServerStateChange":
print("server state change: $data"); 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"]) { if (contact.isGroup == true && contact.server == data["GroupServer"]) {
contact.status = data["ConnectionState"]; 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; break;
default: 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(); typedef ACNEventsFn = Pointer<Utf8> Function();
class CwtchFfi implements Cwtch { class CwtchFfi implements Cwtch {
DynamicLibrary library; late DynamicLibrary library;
CwtchNotifier cwtchNotifier; late CwtchNotifier cwtchNotifier;
Isolate cwtchIsolate; late Isolate cwtchIsolate;
CwtchFfi(CwtchNotifier _cwtchNotifier) { CwtchFfi(CwtchNotifier _cwtchNotifier) {
if (Platform.isWindows) { if (Platform.isWindows) {
@ -79,9 +79,9 @@ class CwtchFfi implements Cwtch {
String bundledTor = ""; String bundledTor = "";
Map<String, String> envVars = Platform.environment; Map<String, String> envVars = Platform.environment;
if (Platform.isLinux) { if (Platform.isLinux) {
home = envVars['HOME']; home = (envVars['HOME'])!;
} else if (Platform.isWindows) { } else if (Platform.isWindows) {
home = envVars['UserProfile']; home = (envVars['UserProfile'])!;
bundledTor = "Tor\\Tor\\tor.exe"; bundledTor = "Tor\\Tor\\tor.exe";
} }
var cwtchDir = path.join(home, ".cwtch/dev/"); 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 // Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it
static Stream<String> pollAppbusEvents() async* { static Stream<String> pollAppbusEvents() async* {
DynamicLibrary library; late DynamicLibrary library;
if (Platform.isWindows) { if (Platform.isWindows) {
library = DynamicLibrary.open("libCwtch.dll"); library = DynamicLibrary.open("libCwtch.dll");
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
@ -356,4 +356,26 @@ class CwtchFfi implements Cwtch {
final u2 = groupHandle.toNativeUtf8(); final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length); 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'; final appbusEventChannelName = 'test.flutter.dev/eventBus';
Future<String> androidLibraryDir; late Future<dynamic> androidLibraryDir;
Future<Directory> androidHomeDirectory; late Future<dynamic> androidHomeDirectory;
CwtchNotifier cwtchNotifier; late CwtchNotifier cwtchNotifier;
CwtchGomobile(CwtchNotifier _cwtchNotifier) { CwtchGomobile(CwtchNotifier _cwtchNotifier) {
print("gomobile.dart: CwtchGomobile()"); print("gomobile.dart: CwtchGomobile()");
@ -73,34 +73,34 @@ class CwtchGomobile implements Cwtch {
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> ACNEvents() { Future<dynamic> ACNEvents() {
return cwtchPlatform.invokeMethod("ACNEvents"); return cwtchPlatform.invokeMethod("ACNEvents");
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> ContactEvents() { Future<dynamic> ContactEvents() {
return cwtchPlatform.invokeMethod("ContactEvents"); return cwtchPlatform.invokeMethod("ContactEvents");
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> GetProfiles() { Future<dynamic> GetProfiles() {
print("gomobile.dart: GetProfiles()"); print("gomobile.dart: GetProfiles()");
return cwtchPlatform.invokeMethod("GetProfiles"); return cwtchPlatform.invokeMethod("GetProfiles");
} }
// ignore: non_constant_identifier_names // 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}); return cwtchPlatform.invokeMethod("NumMessages", {"profile": profile, "contact": handle});
} }
// ignore: non_constant_identifier_names // 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()); print("gomobile.dart GetMessage " + index.toString());
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index}); return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index});
} }
// ignore: non_constant_identifier_names // 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}); 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) { void RejectInvite(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "handle": 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:flutter/foundation.dart';
import 'package:cwtch/cwtch/ffi.dart'; import 'package:cwtch/cwtch/ffi.dart';
import 'package:cwtch/cwtch/gomobile.dart'; import 'package:cwtch/cwtch/gomobile.dart';
@ -35,14 +36,14 @@ class Flwtch extends StatefulWidget {
class FlwtchState extends State<Flwtch> { class FlwtchState extends State<Flwtch> {
final TextStyle biggerFont = const TextStyle(fontSize: 18); final TextStyle biggerFont = const TextStyle(fontSize: 18);
Cwtch cwtch; late Cwtch cwtch;
bool cwtchInit = false; bool cwtchInit = false;
ProfileInfoState selectedProfile; late ProfileInfoState selectedProfile;
String selectedConversation = ""; String selectedConversation = "";
var columns = [1]; // default or 'single column' mode var columns = [1]; // default or 'single column' mode
//var columns = [1, 1, 2]; //var columns = [1, 1, 2];
AppModel appStatus; late AppModel appStatus;
ProfileListState profs; late ProfileListState profs;
@override @override
initState() { initState() {
@ -50,11 +51,15 @@ class FlwtchState extends State<Flwtch> {
cwtchInit = false; cwtchInit = false;
profs = ProfileListState(); profs = ProfileListState();
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus);
if (Platform.isAndroid) { if (Platform.isAndroid) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager());
cwtch = CwtchGomobile(cwtchNotifier); cwtch = CwtchGomobile(cwtchNotifier);
} else if (Platform.isLinux) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, LinuxNotificationsManager());
cwtch = CwtchFfi(cwtchNotifier);
} else { } else {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager());
cwtch = CwtchFfi(cwtchNotifier); cwtch = CwtchFfi(cwtchNotifier);
} }
@ -76,19 +81,24 @@ class FlwtchState extends State<Flwtch> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//appStatus = AppModel(cwtch: cwtch); //appStatus = AppModel(cwtch: cwtch);
globalSettings.initPackageInfo();
return MultiProvider( return MultiProvider(
providers: [getFlwtchStateProvider(), getProfileListProvider(), getSettingsProvider(), getErrorHandlerProvider(), getTorStatusProvider()], providers: [
getFlwtchStateProvider(),
getProfileListProvider(),
getSettingsProvider(),
getErrorHandlerProvider(),
getTorStatusProvider(),
],
builder: (context, widget) { builder: (context, widget) {
Provider.of<Settings>(context).initPackageInfo();
return Consumer<Settings>( return Consumer<Settings>(
builder: (context, opaque, child) => MaterialApp( builder: (context, settings, child) => MaterialApp(
key: Key('app'), key: Key('app'),
locale: Provider.of<Settings>(context).locale, locale: settings.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
title: 'Cwtch', title: 'Cwtch',
theme: mkThemeData(opaque), theme: mkThemeData(settings),
// from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(), // from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(),
// from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(), // from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(),
home: cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ProfileMgrView()) : SplashView(), home: cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ProfileMgrView()) : SplashView(),

View File

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

View File

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

View File

@ -9,7 +9,7 @@ class ServerListState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
ServerInfoState getServer(String onion) { ServerInfoState? getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion); int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null; return idx >= 0 ? _servers[idx] : null;
} }
@ -22,5 +22,5 @@ class ServerInfoState extends ChangeNotifier {
final String onion; final String onion;
final String status; 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() { Color textfieldBackgroundColor() {
return whitePurple; return purple;
} }
Color textfieldBorderColor() { Color textfieldBorderColor() {
@ -722,11 +722,11 @@ class CwtchLight extends OpaqueThemeType {
} }
Color portraitOnlineBorderColor() { Color portraitOnlineBorderColor() {
return darkPurple; return greyPurple;
} }
Color portraitOnlineBackgroundColor() { Color portraitOnlineBackgroundColor() {
return darkPurple; return greyPurple;
} }
Color portraitOnlineTextColor() { Color portraitOnlineTextColor() {
@ -1225,7 +1225,7 @@ class Opaque extends OpaqueThemeType {
return sidePaneMinSize() + chatPaneMinSize(); return sidePaneMinSize() + chatPaneMinSize();
} }
static OpaqueThemeType _current; static late OpaqueThemeType _current;
static final OpaqueThemeType dark = CwtchDark(); static final OpaqueThemeType dark = CwtchDark();
static final OpaqueThemeType light = CwtchLight(); static final OpaqueThemeType light = CwtchLight();
static void setDark() { static void setDark() {
@ -1345,16 +1345,26 @@ ThemeData mkThemeData(Settings opaque) {
return ThemeData( return ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
primarySwatch: Colors.red, primarySwatch: Colors.red,
primaryIconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
primaryColor: opaque.current().backgroundMainColor(), primaryColor: opaque.current().backgroundMainColor(),
canvasColor: opaque.current().backgroundPaneColor(), canvasColor: opaque.current().backgroundPaneColor(),
accentColor: opaque.current().defaultButtonColor(),
buttonColor: opaque.current().defaultButtonColor(),
backgroundColor: opaque.current().backgroundMainColor(), backgroundColor: opaque.current().backgroundMainColor(),
highlightColor: opaque.current().hilightElementTextColor(), highlightColor: opaque.current().hilightElementTextColor(),
iconTheme: IconThemeData( iconTheme: IconThemeData(
color: opaque.current().mainTextColor(), color: opaque.current().mainTextColor(),
), ),
cardColor: opaque.current().backgroundMainColor(), cardColor: opaque.current().backgroundMainColor(),
appBarTheme: AppBarTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(
color: opaque.current().mainTextColor(),
),
actionsIconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
),
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()), backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
@ -1364,11 +1374,16 @@ ThemeData mkThemeData(Settings opaque) {
), ),
elevatedButtonTheme: ElevatedButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()), backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()), foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()), overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))), 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( dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor(), backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(color: opaque.current().mainTextColor()), titleTextStyle: TextStyle(color: opaque.current().mainTextColor()),
@ -1387,5 +1402,13 @@ ThemeData mkThemeData(Settings opaque) {
caption: TextStyle(color: opaque.current().mainTextColor()), caption: TextStyle(color: opaque.current().mainTextColor()),
button: TextStyle(color: opaque.current().mainTextColor()), button: TextStyle(color: opaque.current().mainTextColor()),
overline: 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. /// Settings Pane.
class Settings extends ChangeNotifier { class Settings extends ChangeNotifier {
Locale locale; Locale locale;
PackageInfo packageInfo; late PackageInfo packageInfo;
OpaqueThemeType theme; OpaqueThemeType theme;
bool experimentsEnabled; late bool experimentsEnabled;
HashMap<String, bool> experiments = HashMap.identity(); HashMap<String, bool> experiments = HashMap.identity();
bool blockUnknownConnections; late bool blockUnknownConnections;
/// Set the dark theme. /// Set the dark theme.
void setDark() { void setDark() {
@ -112,11 +112,13 @@ class Settings extends ChangeNotifier {
/// Turn on a specific experiment. /// Turn on a specific experiment.
enableExperiment(String key) { enableExperiment(String key) {
experiments.update(key, (value) => true, ifAbsent: () => true); experiments.update(key, (value) => true, ifAbsent: () => true);
notifyListeners();
} }
/// Turn off a specific experiment /// Turn off a specific experiment
disableExperiment(String key) { disableExperiment(String key) {
experiments.update(key, (value) => false, ifAbsent: () => false); experiments.update(key, (value) => false, ifAbsent: () => false);
notifyListeners();
} }
/// Construct a default settings object. /// Construct a default settings object.

View File

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

View File

@ -26,7 +26,6 @@ class AddContactView extends StatefulWidget {
class _AddContactViewState extends State<AddContactView> { class _AddContactViewState extends State<AddContactView> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _createGroupFormKey = GlobalKey<FormState>(); final _createGroupFormKey = GlobalKey<FormState>();
final _joinGroupFormKey = GlobalKey<FormState>();
final ctrlrOnion = TextEditingController(text: ""); final ctrlrOnion = TextEditingController(text: "");
final ctrlrContact = TextEditingController(text: ""); final ctrlrContact = TextEditingController(text: "");
final ctrlrGroupName = TextEditingController(text: ""); final ctrlrGroupName = TextEditingController(text: "");
@ -34,9 +33,14 @@ class _AddContactViewState extends State<AddContactView> {
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).titleManageContacts), title: Text(AppLocalizations.of(context)!.titleManageContacts),
), ),
body: _buildForm(), body: _buildForm(),
); );
@ -46,15 +50,20 @@ class _AddContactViewState extends State<AddContactView> {
ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion; ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion;
/// We display a different number of tabs dependening on the experiment setup /// 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 Consumer<ErrorHandler>(builder: (context, globalErrorHandler, child) {
return DefaultTabController( return DefaultTabController(
length: groupsEnabled ? 4 : 1, length: groupsEnabled ? 2 : 1,
child: Column(children: [ child: Column(children: [
(groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()), (groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()),
Expanded( Expanded(
child: TabBarView( 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() { void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion)); 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
@ -72,7 +81,7 @@ class _AddContactViewState extends State<AddContactView> {
tabs: [ tabs: [
Tab( Tab(
icon: Icon(Icons.person_add_rounded), 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: [ tabs: [
Tab( Tab(
icon: Icon(Icons.person_add_rounded), 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.backup), text: AppLocalizations.of(context)!.titleManageServers),
Tab(icon: Icon(Icons.group), text: AppLocalizations.of(context).createGroup), Tab(icon: Icon(Icons.group), text: AppLocalizations.of(context)!.createGroup),
Tab(icon: Icon(Icons.group_add), text: AppLocalizations.of(context).joinGroup),
], ],
); );
} }
@ -103,7 +111,7 @@ class _AddContactViewState extends State<AddContactView> {
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
key: _formKey, key: _formKey,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context).profileOnionLabel), CwtchLabel(label: AppLocalizations.of(context)!.profileOnionLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -111,12 +119,12 @@ class _AddContactViewState extends State<AddContactView> {
controller: ctrlrOnion, controller: ctrlrOnion,
onPressed: _copyOnion, onPressed: _copyOnion,
icon: Icon(Icons.copy), icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context).copyBtn, tooltip: AppLocalizations.of(context)!.copyBtn,
), ),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context).pasteAddressToAddContact), CwtchLabel(label: AppLocalizations.of(context)!.pasteAddressToAddContact),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -127,29 +135,25 @@ class _AddContactViewState extends State<AddContactView> {
return null; return null;
} }
if (globalErrorHandler.invalidImportStringError) { if (globalErrorHandler.invalidImportStringError) {
return AppLocalizations.of(context).invalidImportString; return AppLocalizations.of(context)!.invalidImportString;
} else if (globalErrorHandler.contactAlreadyExistsError) { } else if (globalErrorHandler.contactAlreadyExistsError) {
return AppLocalizations.of(context).contactAlreadyExists; return AppLocalizations.of(context)!.contactAlreadyExists;
} else if (globalErrorHandler.explicitAddContactSuccess) {} } else if (globalErrorHandler.explicitAddContactSuccess) {}
return null; return null;
}, },
onChanged: (String peerAddr) async { onChanged: (String importBundle) async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
final setPeerAttribute = { Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, importBundle);
"EventType": "AddContact",
"Data": {"ImportString": peerAddr},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (globalErrorHandler.explicitAddContactSuccess) { if (globalErrorHandler.importBundleSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).successfullAddedContact + peerAddr)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + importBundle));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context); 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."); 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( return Container(
margin: EdgeInsets.all(30), margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
@ -176,80 +175,59 @@ class _AddContactViewState extends State<AddContactView> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CwtchLabel(label: AppLocalizations.of(context).server), CwtchLabel(label: AppLocalizations.of(context)!.server),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
DropdownButton( DropdownButton(
onChanged: (newServer) { onChanged: (String? newServer) {
server = newServer; setState(() {
server = newServer!;
});
}, },
isExpanded: true, // magic property
value: server, value: server,
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) { items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: serverInfo.onion, value: serverInfo.onion,
child: Text(serverInfo.onion), child: Text(
serverInfo.onion,
overflow: TextOverflow.ellipsis,
),
); );
}).toList()), }).toList()),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context).groupName), CwtchLabel(label: AppLocalizations.of(context)!.groupName),
SizedBox( SizedBox(
height: 20, 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( SizedBox(
height: 20, height: 20,
), ),
ElevatedButton( ElevatedButton(
onPressed: () {}, onPressed: () {
child: Text(AppLocalizations.of(context).createGroupBtn), 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 /// TODO Manage Servers Tab
Widget manageServersTab() { Widget manageServersTab() {
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) { final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) {

View File

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

View File

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

View File

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

View File

@ -42,7 +42,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( 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(), body: _buildSettingsList(),
); );
@ -60,40 +60,15 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
minHeight: viewportConstraints.maxHeight, minHeight: viewportConstraints.maxHeight,
), ),
child: Container( child: Container(
margin: EdgeInsets.all(30), margin: EdgeInsets.all(10),
padding: EdgeInsets.all(20), padding: EdgeInsets.all(2),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ 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 // Nickname Save Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context).displayNameLabel), CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -105,21 +80,70 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text; Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetGroupAttribute(profileOnion, handle, "local.name", ctrlrNick.text); Provider.of<FlwtchState>(context, listen: false).cwtch.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), 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: [ Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox( SizedBox(
height: 20, 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( SizedBox(
height: 20, height: 20,
), ),
// TODO // 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() { void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ContactInfoState>(context, listen: false).onion)); 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); 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 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/views/peersettingsview.dart'; import 'package:cwtch/views/peersettingsview.dart';
import 'package:cwtch/widgets/DropdownContacts.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); ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
@ -94,7 +95,7 @@ class _MessageViewState extends State<MessageView> {
_sendMessageHelper(); _sendMessageHelper();
} }
void _sendInvitation([String ignoredParam]) { void _sendInvitation([String? ignoredParam]) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, this.selectedContact); .SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, this.selectedContact);
@ -114,37 +115,36 @@ class _MessageViewState extends State<MessageView> {
Widget _buildComposeBox() { Widget _buildComposeBox() {
return Container( return Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor(), color: Provider.of<Settings>(context).theme.backgroundMainColor(),
padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2),
height: 100, height: 100,
padding: EdgeInsets.all(8.0),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: TextField( child: Container(
key: Key('txtCompose'), decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor()))),
controller: ctrlrCompose, child: TextFormField(
focusNode: focusNode, key: Key('txtCompose'),
textInputAction: TextInputAction.send, controller: ctrlrCompose,
onSubmitted: _sendMessage, autofocus: true,
)), focusNode: focusNode,
Column(children: [ textInputAction: TextInputAction.send,
SizedBox( onFieldSubmitted: _sendMessage,
width: 100, decoration: InputDecoration(
height: 50, enabledBorder: InputBorder.none,
child: Padding( focusedBorder: InputBorder.none,
padding: EdgeInsets.fromLTRB(2, 2, 2, 2), enabled: true,
child: ElevatedButton( prefixIcon: IconButton(
child: Icon(Icons.send, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()), icon: Icon(Icons.insert_invitation, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
style: ButtonStyle( tooltip: "Send a contact or group invite",
fixedSize: MaterialStateProperty.all(Size(86, 50)), onPressed: () => _modalSendInvitation(context)),
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.defaultButtonColor()), 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, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text(AppLocalizations.of(bcontext).invitationLabel), Text(AppLocalizations.of(bcontext)!.invitationLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -183,7 +183,7 @@ class _MessageViewState extends State<MessageView> {
height: 20, height: 20,
), ),
ElevatedButton( ElevatedButton(
child: Text(AppLocalizations.of(bcontext).inviteBtn, semanticsLabel: AppLocalizations.of(bcontext).inviteBtn), child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn),
onPressed: () { onPressed: () {
this._sendInvitation(); this._sendInvitation();
Navigator.pop(bcontext); Navigator.pop(bcontext);

View File

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

View File

@ -10,6 +10,7 @@ import 'package:cwtch/widgets/profilerow.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../opaque.dart';
import 'addeditprofileview.dart'; import 'addeditprofileview.dart';
import 'globalsettingsview.dart'; import 'globalsettingsview.dart';
@ -31,36 +32,52 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Prevents Android back button from closing the app on the profile manager screen return Consumer<Settings>(
// (which would shutdown connections and all kinds of other expensive to generate things) // Prevents Android back button from closing the app on the profile manager screen
// TODO pop up a dialogue regarding closing the app? // (which would shutdown connections and all kinds of other expensive to generate things)
return new WillPopScope( // TODO pop up a dialogue regarding closing the app?
onWillPop: () async => false, builder: (context, settings, child) => WillPopScope(
child: Scaffold( onWillPop: () async => false,
backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor(), child: Scaffold(
appBar: AppBar( backgroundColor: settings.theme.backgroundMainColor(),
title: Text(AppLocalizations.of(context).titleManageProfiles), appBar: AppBar(
actions: [ title: Row(children: [
IconButton(icon: TorIcon(), onPressed: _pushTorStatus), Image(
IconButton(icon: Icon(Icons.bug_report_outlined), onPressed: _setLoggingLevelDebug), image: AssetImage("assets/core/knott-white.png"),
IconButton( filterQuality: FilterQuality.medium,
icon: Icon(Icons.lock_open), isAntiAlias: true,
tooltip: AppLocalizations.of(context).tooltipUnlockProfiles, width: 32,
onPressed: _modalUnlockProfiles, height: 32,
), colorBlendMode: BlendMode.dstIn,
IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context).tooltipOpenSettings, onPressed: _pushGlobalSettings), color: Provider.of<Settings>(context).theme.backgroundHilightElementColor(),
], ),
), SizedBox(
floatingActionButton: FloatingActionButton( width: 10,
onPressed: _pushAddEditProfile, ),
tooltip: AppLocalizations.of(context).addNewProfileBtn, Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor())))
child: Icon( ]),
Icons.add, actions: [
semanticLabel: AppLocalizations.of(context).addNewProfileBtn, 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(
body: _buildProfileManager(), //_buildSuggestions(), onPressed: _pushAddEditProfile,
)); tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
),
),
body: _buildProfileManager(),
)),
);
} }
void _setLoggingLevelDebug() { void _setLoggingLevelDebug() {
@ -112,55 +129,83 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
void _modalUnlockProfiles() { void _modalUnlockProfiles() {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
isScrollControlled: true,
builder: (BuildContext context) { builder: (BuildContext context) {
return Container( return Padding(
height: 200, // bespoke value courtesy of the [TextField] docs padding: MediaQuery.of(context).viewInsets,
child: Center( child: RepaintBoundary(
child: Padding( child: Container(
padding: EdgeInsets.all(10.0), height: 200, // bespoke value courtesy of the [TextField] docs
child: Column( child: Center(
mainAxisAlignment: MainAxisAlignment.center, child: Padding(
mainAxisSize: MainAxisSize.min, padding: EdgeInsets.all(10.0),
children: <Widget>[ child: Column(
Text(AppLocalizations.of(context).enterProfilePassword), mainAxisAlignment: MainAxisAlignment.center,
SizedBox( mainAxisSize: MainAxisSize.min,
height: 20, children: <Widget>[
), Text(AppLocalizations.of(context)!.enterProfilePassword),
CwtchPasswordField( SizedBox(
controller: ctrlrPassword, height: 20,
), ),
SizedBox( CwtchPasswordField(
height: 20, autofocus: true,
), controller: ctrlrPassword,
ElevatedButton( action: unlock,
child: Text(AppLocalizations.of(context).unlock, semanticsLabel: AppLocalizations.of(context).unlock), validator: (value) {},
onPressed: () { ),
Provider.of<FlwtchState>(context, listen: false).cwtch.LoadProfiles(ctrlrPassword.value.text); SizedBox(
ctrlrPassword.text = ""; height: 20,
Navigator.pop(context); ),
}, 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() { Widget _buildProfileManager() {
final tiles = Provider.of<ProfileListState>(context).profiles.map( return Consumer<ProfileListState>(
(ProfileInfoState profile) { builder: (context, pls, child) {
return ChangeNotifierProvider<ProfileInfoState>.value( final tiles = pls.profiles.map(
value: profile, (ProfileInfoState profile) {
builder: (context, child) => ProfileRow(), 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 { class SplashView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print("SplashView build()"); return const Scaffold(
return Scaffold( body: const Center(child: const Text("Loading Cwtch...")),
appBar: AppBar(title: Text("Cwtch")),
body: Center(child: Column(children: <Widget>[Text("Loading Cwtch...")])),
); );
} }
} }

View File

@ -44,7 +44,7 @@ class _TorStatusView extends State<TorStatusView> {
ListTile( ListTile(
leading: TorIcon(), leading: TorIcon(),
title: Text("Tor Status"), 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( trailing: ElevatedButton(
child: Text("Reset"), child: Text("Reset"),
onPressed: () { onPressed: () {

View File

@ -9,7 +9,7 @@ import '../model.dart';
// Pass an onChanged handler to access value // Pass an onChanged handler to access value
class DropdownContacts extends StatefulWidget { class DropdownContacts extends StatefulWidget {
DropdownContacts({ DropdownContacts({
this.onChanged, required this.onChanged,
}); });
final Function(dynamic) onChanged; final Function(dynamic) onChanged;
@ -18,22 +18,20 @@ class DropdownContacts extends StatefulWidget {
} }
class _DropdownContactsState extends State<DropdownContacts> { class _DropdownContactsState extends State<DropdownContacts> {
String selected; String? selected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DropdownButton( return DropdownButton(
value: this.selected, value: this.selected,
items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.map<DropdownMenuItem<String>>((ContactInfoState contact) { items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.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(), }).toList(),
onChanged: (newVal) { onChanged: (String? newVal) {
setState(() { setState(() {
this.selected = newVal; 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. // Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchButtonTextField extends StatefulWidget { 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 TextEditingController controller;
final Function onPressed; final Function()? onPressed;
final Icon icon; final Icon icon;
final String tooltip; final String tooltip;
final bool readonly; final bool readonly;
@ -37,6 +37,7 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
), ),
floatingLabelBehavior: FloatingLabelBehavior.never, floatingLabelBehavior: FloatingLabelBehavior.never,
filled: true, filled: true,
fillColor: theme.current().textfieldBackgroundColor(),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)), 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)), 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(), color: theme.current().textfieldErrorColor(),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
fillColor: theme.current().textfieldBackgroundColor(),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
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) { 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; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (BuildContext builderContext) { 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( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider.value(value: profile), 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) { String dateToNiceString(DateTime date) {
if (date.millisecondsSinceEpoch == 0) { 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 // Callers must provide a label text
// TODO: Integrate this with a settings "zoom" / accessibility setting // TODO: Integrate this with a settings "zoom" / accessibility setting
class CwtchLabel extends StatefulWidget { class CwtchLabel extends StatefulWidget {
CwtchLabel({this.label}); CwtchLabel({required this.label});
final String label; final String label;
@override @override

View File

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

View File

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

View File

@ -15,41 +15,42 @@ class _MessageListState extends State<MessageList> {
@override @override
Widget build(BuildContext outerContext) { Widget build(BuildContext outerContext) {
return Card( return RepaintBoundary(
child: Scrollbar( child: Container(
isAlwaysShown: true, child: Scrollbar(
controller: ctrlr1, isAlwaysShown: true,
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, controller: ctrlr1,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages, child: Container(
reverse: true, // Only show broken heart is the contact is offline...
itemBuilder: (itemBuilderContext, index) { decoration: BoxDecoration(
var trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1; image: Provider.of<ContactInfoState>(outerContext).isOnline()
return ChangeNotifierProvider( ? null
key: ValueKey(trueIndex), : DecorationImage(
create: (x) => MessageState( fit: BoxFit.contain,
context: itemBuilderContext, image: AssetImage("assets/core/negative_heart_512px.png"),
profileOnion: Provider.of<ProfileInfoState>(outerContext, listen: false).onion, colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.mainTextColor(), BlendMode.srcIn))),
contactHandle: Provider.of<ContactInfoState>(x, listen: false).onion, child: ListView.builder(
messageIndex: trueIndex, controller: ctrlr1,
), itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
builder: (bcontext, child) { reverse: true,
String idx = Provider.of<ContactInfoState>(outerContext).isGroup == true && Provider.of<MessageState>(bcontext).signature.isEmpty == false itemBuilder: (itemBuilderContext, index) {
? Provider.of<MessageState>(bcontext).signature var trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
: trueIndex.toString(); return ChangeNotifierProvider(
return MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); 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'; import 'messageloadingbubble.dart';
class MessageRow extends StatefulWidget { class MessageRow extends StatefulWidget {
MessageRow({Key key}) : super(key: key); MessageRow({Key? key}) : super(key: key);
@override @override
_MessageRowState createState() => _MessageRowState(); _MessageRowState createState() => _MessageRowState();
@ -52,10 +52,12 @@ class _MessageRowState extends State<MessageRow> {
child: Padding( child: Padding(
padding: EdgeInsets.all(4.0), padding: EdgeInsets.all(4.0),
child: ProfileImage( child: ProfileImage(
diameter: 48.0, diameter: 48.0,
imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath, imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated", //maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()))); border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
badgeTextColor: Colors.red, badgeColor: Colors.red,
)));
widgetRow = <Widget>[ widgetRow = <Widget>[
wdgPortrait, wdgPortrait,
@ -76,7 +78,7 @@ class _MessageRowState extends State<MessageRow> {
case 101: case 101:
return InvitationBubble(); return InvitationBubble();
} }
return null; return MalformedBubble();
} }
void _btnAdd() { void _btnAdd() {
@ -94,7 +96,7 @@ class _MessageRowState extends State<MessageRow> {
final setPeerAttributeJson = jsonEncode(setPeerAttribute); final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson); 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); 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. // Provides a styled Password Input Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchPasswordField extends StatefulWidget { 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 TextEditingController controller;
final FormFieldValidator validator; final FormFieldValidator validator;
final Function(String)? action;
final bool autofocus;
@override @override
_CwtchTextFieldState createState() => _CwtchTextFieldState(); _CwtchTextFieldState createState() => _CwtchTextFieldState();
} }
class _CwtchTextFieldState extends State<CwtchPasswordField> { class _CwtchTextFieldState extends State<CwtchPasswordField> {
bool obscureText = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// todo: translations
var label = "View Password";
if (!obscureText) {
label = "Hide Password";
}
return Consumer<Settings>(builder: (context, theme, child) { return Consumer<Settings>(builder: (context, theme, child) {
return TextFormField( return TextFormField(
autofocus: widget.autofocus,
controller: widget.controller, controller: widget.controller,
validator: widget.validator, validator: widget.validator,
obscureText: true, obscureText: obscureText,
onFieldSubmitted: widget.action,
textInputAction: TextInputAction.unspecified,
enableSuggestions: false, enableSuggestions: false,
autocorrect: false, autocorrect: false,
decoration: InputDecoration( 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( errorStyle: TextStyle(
color: theme.current().textfieldErrorColor(), color: theme.current().textfieldErrorColor(),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -35,7 +59,6 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
fillColor: theme.current().textfieldBackgroundColor(), fillColor: theme.current().textfieldBackgroundColor(),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
), ),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
); );
}); });
} }

View File

@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
import '../settings.dart'; import '../settings.dart';
class ProfileImage extends StatefulWidget { 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 double diameter;
final String imagePath; final String imagePath;
final Color border; final Color border;
@ -21,7 +21,8 @@ class ProfileImage extends StatefulWidget {
class _ProfileImageState extends State<ProfileImage> { class _ProfileImageState extends State<ProfileImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack(children: [ return RepaintBoundary(
child: Stack(children: [
ClipOval( ClipOval(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Container( 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)), 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( IconButton(
enableFeedback: true, 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()), icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () { onPressed: () {
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath); _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) { void _pushContactList(ProfileInfoState profile, bool includeDoublePane) {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
settings: RouteSettings(name: "conversations"),
builder: (BuildContext buildcontext) { builder: (BuildContext buildcontext) {
return MultiProvider( return MultiProvider(
providers: [ providers: [

View File

@ -7,10 +7,10 @@ doNothing(String x) {}
// Provides a styled Text Field for use in Form Widgets. // Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchTextField extends StatefulWidget { 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 TextEditingController controller;
final String labelText; final String labelText;
final FormFieldValidator validator; final FormFieldValidator? validator;
final Function(String) onChanged; final Function(String) onChanged;
@override @override
@ -40,7 +40,6 @@ class _CwtchTextFieldState extends State<CwtchTextField> {
fillColor: theme.current().textfieldBackgroundColor(), fillColor: theme.current().textfieldBackgroundColor(),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
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> { class _TorIconState extends State<TorIcon> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Image( return RepaintBoundary(
child: Image(
image: AssetImage(Provider.of<TorStatus>(context).progress == 0 image: AssetImage(Provider.of<TorStatus>(context).progress == 0
? "assets/core/Tor_OFF.png" ? "assets/core/Tor_OFF.png"
: (Provider.of<TorStatus>(context).progress == 100 ? "assets/core/Tor_icon.png" : "assets/core/Tor_Booting_up.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(), color: Provider.of<Settings>(context).theme.mainTextColor(),
colorBlendMode: BlendMode.srcIn, colorBlendMode: BlendMode.srcIn,
semanticLabel: Provider.of<TorStatus>(context).progress == 100 semanticLabel: Provider.of<TorStatus>(context).progress == 100
? AppLocalizations.of(context).networkStatusOnline ? AppLocalizations.of(context)!.networkStatusOnline
: (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context).networkStatusDisconnected : AppLocalizations.of(context).networkStatusAttemptingTor), : (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(), stream: Provider.of<FlwtchState>(context).appStatus.torStatus(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) { builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return Text( return Text(
snapshot.hasData ? snapshot.data : AppLocalizations.of(context).loadingTor, snapshot.hasData ? snapshot.data! : AppLocalizations.of(context)!.loadingTor,
style: Theme.of(context).textTheme.headline4, 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 "generated_plugin_registrant.h"
#include <window_size/window_size_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { 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 list(APPEND FLUTTER_PLUGIN_LIST
window_size
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -8,6 +8,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -63,7 +70,21 @@ packages:
name: cupertino_icons name: cupertino_icons
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -84,7 +105,7 @@ packages:
name: file name: file
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.1.0" version: "6.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -128,7 +149,7 @@ packages:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.13.1" version: "0.13.3"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -184,42 +205,42 @@ packages:
name: package_info_plus name: package_info_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
package_info_plus_linux: package_info_plus_linux:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_linux name: package_info_plus_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.2"
package_info_plus_macos: package_info_plus_macos:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_macos name: package_info_plus_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.1.1"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
package_info_plus_web: package_info_plus_web:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_web name: package_info_plus_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
package_info_plus_windows: package_info_plus_windows:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_windows name: package_info_plus_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -261,7 +282,7 @@ packages:
name: path_provider_windows name: path_provider_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.1"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@ -269,6 +290,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.11.0" version: "1.11.0"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -296,7 +324,7 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.3.2+3" version: "5.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -385,7 +413,16 @@ packages:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -393,6 +430,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
sdks: sdks:
dart: ">=2.12.0 <3.0.0" dart: ">=2.13.0 <3.0.0"
flutter: ">=1.20.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 version: 1.0.0+1
environment: environment:
sdk: ">=2.7.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
provider: "4.3.2+3" provider: 5.0.0
package_info_plus: ^1.0.0 package_info_plus: ^1.0.0
#intl_translation: any #intl_translation: any
flutter_localizations: flutter_localizations:
@ -34,6 +34,7 @@ dependencies:
cupertino_icons: ^1.0.0 cupertino_icons: ^1.0.0
ffi: ^1.0.0 ffi: ^1.0.0
path_provider: ^2.0.0 path_provider: ^2.0.0
desktop_notifications: 0.5.0
glob: any glob: any
# todo: flutter_driver causes version conflict. eg https://github.com/flutter/flutter/issues/44829 # todo: flutter_driver causes version conflict. eg https://github.com/flutter/flutter/issues/44829
@ -44,6 +45,12 @@ dependencies:
flutter_driver: flutter_driver:
sdk: flutter sdk: flutter
window_size:
git:
url: git://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81
#dev_dependencies: #dev_dependencies:
# flutter_lokalise: any # 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( home: Card(child: CwtchButtonTextField(
icon: Icon(Icons.bug_report_outlined), icon: Icon(Icons.bug_report_outlined),
tooltip: testingStr, 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); tester.binding.window.physicalSizeTestValue = Size(800, 300);
final TextEditingController ctrlr1 = TextEditingController(); final TextEditingController ctrlr1 = TextEditingController();
Widget testWidget = CwtchTextField(controller: ctrlr1); Widget testWidget = CwtchTextField(controller: ctrlr1, validator: (value) { }, labelText: '',);
Widget testHarness = MultiProvider( Widget testHarness = MultiProvider(
providers:[getSettingsEnglishDark()], providers:[getSettingsEnglishDark()],
@ -76,7 +76,7 @@ void main() {
if (number == null) return strFail2; if (number == null) return strFail2;
return null; return null;
}, },
onChanged: (value) => formKey.currentState.validate(), onChanged: (value) => formKey.currentState!.validate(),
); );
Widget testHarness = MultiProvider( Widget testHarness = MultiProvider(
@ -125,7 +125,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// empty string // 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 tester.pumpAndSettle();
await expectLater(find.byWidget(testHarness), matchesGoldenFile(file('form_final'))); await expectLater(find.byWidget(testHarness), matchesGoldenFile(file('form_final')));
expect(find.text(strFail1), findsOneWidget); expect(find.text(strFail1), findsOneWidget);