Compare commits

..

58 Commits

Author SHA1 Message Date
Muhammad Hamza 02b4a73de7 web QR dummy screen added 2021-10-19 17:04:51 +05:00
Muhammad Hamza 0f0a647c3c Merge branch 'flutter-app' of https://github.com/simplex-chat/simplex-chat into flutter-app
Some msg here
2021-10-19 16:13:14 +05:00
Muhammad Hamza 86de81adb3 bug fixes for mobile view 2021-10-19 16:13:01 +05:00
Evgeny Poberezkin 7d32be57a9 remove unused import 2021-10-19 08:31:45 +01:00
Evgeny Poberezkin bc1c8589c3 update format 2021-10-19 08:16:49 +01:00
Muhammad Hamza 6b49e1d5e4 dfdserge branch 'flutter-app' of https://github.com/simplex-chat/simplex-chat into flutter-app 2021-10-19 10:20:20 +05:00
Muhammad Hamza 7790eea069 UI fixes + Scan fix 2021-10-19 10:19:41 +05:00
Evgeny Poberezkin dd9d990e3f skip flutter test 2021-10-18 19:22:42 +01:00
Muhammad Hamza 504e0af11a Icon fixed 2021-10-18 23:15:36 +05:00
Muhammad Hamza 7248e6096d fixed the responsive issues + other 2021-10-18 22:11:31 +05:00
Evgeny Poberezkin c183c819fc update code formatting 2021-10-18 07:46:01 +01:00
Muhammad Hamza 28122fa003 comments added for better understanding 2021-10-17 15:35:30 +05:00
Muhammad Hamza e2e9f78705 bug report icon removed from invitation view 2021-10-16 11:02:20 +05:00
Muhammad Hamza 49444b3056 more options added in user 2021-10-16 11:01:55 +05:00
Muhammad Hamza 4e8ac3fafb Some pull 2021-10-15 11:46:39 +05:00
Muhammad Hamza ff6cd50cc5 logo fix + bug fixes + updates 2021-10-15 11:43:56 +05:00
Muhammad Hamza 67df1c2b7b logo updates and other changes 2021-10-15 11:43:40 +05:00
Muhammad Hamza c464724bc8 updates 2021-10-15 11:43:25 +05:00
Muhammad Hamza 6af87aa69c bug fixes 2021-10-15 11:43:13 +05:00
Evgeny Poberezkin fad1873369 fix module import 2021-10-14 19:45:46 +01:00
Muhammad Hamza ee533bd76a Merge branch 'flutter-app' of https://github.com/simplex-chat/simplex-chat into flutter-app
Pull before 2.12 dark sdk
2021-10-14 10:00:40 +05:00
Muhammad Hamza 6b5d4c3958 null safe code + latest dart SDK 2021-10-14 10:00:07 +05:00
Evgeny Poberezkin 4c81a280c8 fix imports 2021-10-13 07:35:49 +01:00
Muhammad Hamza 368f9ef617 changes as per asked | app icon | removal of chats/groups separation etc 2021-10-13 01:58:29 +05:00
Muhammad Hamza deeda09ca3 group conversation + contact enabled 2021-10-11 20:43:08 +05:00
Muhammad Hamza 28a70074c8 edit profile available 2021-10-11 20:42:28 +05:00
Muhammad Hamza 0b7c618f33 fixes 2021-10-11 20:42:18 +05:00
Muhammad Hamza 0118d37232 add members + remove members 2021-10-11 20:42:08 +05:00
Muhammad Hamza 4f5ba12f2d overall bug fixes + UI changes 2021-10-11 14:57:05 +05:00
Muhammad Hamza 2275b582b4 splash added + login persistant 2021-10-10 16:30:28 +05:00
Muhammad Hamza f635df038a new animation added 2021-10-10 16:30:14 +05:00
Muhammad Hamza 36c3acdf2c local storage + ui fixes 2021-10-10 16:30:01 +05:00
Muhammad Hamza 6704fc7cbb changes 2021-10-10 10:55:12 +05:00
Muhammad Hamza 34f345c237 invites view added 2021-10-10 10:54:47 +05:00
Muhammad Hamza 127b798acd conversation view added 2021-10-10 10:54:36 +05:00
Muhammad Hamza c2e39982a1 drawer providers 2021-10-10 10:54:22 +05:00
Muhammad Hamza e1d5e29365 group added 2021-10-10 10:54:02 +05:00
Muhammad Hamza 70dbf3966d changes 2021-10-09 22:37:33 +05:00
Muhammad Hamza 493ea01f85 new screens added 2021-10-09 22:32:22 +05:00
Muhammad Hamza cf0cc8b305 changes 2021-10-09 22:32:01 +05:00
Muhammad Hamza 46e292193c message bubble in chat view 2021-10-09 22:31:50 +05:00
Muhammad Hamza 19d429d739 contact model for dummy working 2021-10-09 22:31:40 +05:00
Muhammad Hamza 451294ba08 fixes 2021-10-09 22:31:25 +05:00
Muhammad Hamza f3182675de file name changes 2021-10-09 22:31:08 +05:00
Muhammad Hamza 80a0a4ef63 chat view added 2021-10-09 22:30:48 +05:00
Evgeny Poberezkin 23117fd96f remove deprecated accentColor 2021-10-09 18:13:40 +01:00
Evgeny Poberezkin d6b6fde480 format code 2021-10-09 17:21:10 +01:00
Evgeny Poberezkin 44892e5b03 Merge branch 'v5' into flutter-app 2021-10-09 13:13:27 +01:00
Evgeny Poberezkin 268e794f5d remove unused assets 2021-10-08 21:16:49 +01:00
Evgeny Poberezkin 7f88684d8c linter warning 2021-10-08 21:12:22 +01:00
Evgeny Poberezkin 17b4b62133 make linter happier 2021-10-08 09:37:26 +01:00
Evgeny Poberezkin 4d3b33d877 remove duplicate simplex.md 2021-10-08 07:37:01 +01:00
Evgeny Poberezkin 1916d4f922 remove haskell folder 2021-10-08 07:34:28 +01:00
Evgeny Poberezkin 3ea1f05460 revert linter changes 2021-10-08 07:30:42 +01:00
Evgeny Poberezkin 1eaa306a70 revert README change 2021-10-08 07:29:12 +01:00
Evgeny Poberezkin 9fa683e7e4 remove extra files 2021-10-08 07:24:48 +01:00
mhmzdev 2292013507 initial push 2021-10-07 17:44:05 +05:00
mhmzdev 13b60b92bf first push flutter simplex chat app 2021-10-07 09:28:59 +05:00
96 changed files with 4094 additions and 810 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ include: package:flutter_lints/flutter.yaml
linter:
# https://dart-lang.github.io/linter/lints/index.html.
rules:
prefer_double_quotes: true
prefer_single_quotes: true
always_declare_return_types: true
avoid_dynamic_calls: true
avoid_empty_else: true
@@ -42,4 +42,4 @@ linter:
unrelated_type_equality_checks: true
unsafe_html: true
use_full_hex_values_for_flutter_colors: true
valid_regexps: true
valid_regexps: true
@@ -43,8 +43,8 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "chat.simplex.app"
minSdkVersion 16
applicationId "com.example.simplex_chat"
minSdkVersion 20
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.simplex.app">
package="com.example.simplex_chat">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.simplex.app">
package="com.example.simplex_chat">
<application
android:label="simplex_chat"
android:label="SimpleX Chat"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
@@ -1,4 +1,4 @@
package chat.simplex.app
package com.example.simplex_chat
import io.flutter.embedding.android.FlutterActivity
Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.simplex.app">
package="com.example.simplex_chat">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
+2 -2
View File
@@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.5.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg width="815" height="233" viewBox="0 0 815 233" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5683 66.9141C43.9954 66.9141 47.376 67.2323 50.7104 67.8687C54.0448 68.5051 57.2403 69.3234 60.2968 70.3235C63.3533 71.3236 66.2014 72.4601 68.8411 73.7329C71.4809 75.0058 73.9122 76.2786 76.1351 77.5515L66.132 96.0987L65.9952 95.9474C65.6001 95.5596 64.6732 94.9282 63.2144 94.0531C61.5472 93.053 59.4632 92.0074 56.9624 90.9164C54.4616 89.8254 51.7293 88.8708 48.7654 88.0525C45.8015 87.2342 42.8376 86.8251 39.8737 86.8251C31.723 86.8251 27.6476 89.5072 27.6476 94.8713C27.6476 96.5079 28.0876 97.8716 28.9675 98.9627C29.8474 100.054 31.1441 101.031 32.8576 101.895C34.5711 102.758 36.7245 103.554 39.318 104.281C41.9114 105.009 44.9216 105.827 48.3486 106.736C53.0723 108.009 57.3329 109.395 61.1304 110.896C64.9279 112.396 68.1465 114.26 70.7862 116.487C73.4259 118.715 75.4636 121.419 76.8993 124.602C78.3349 127.784 79.0527 131.602 79.0527 136.057C79.0527 141.512 78.0107 146.126 75.9267 149.899C73.8427 153.673 71.0872 156.718 67.6602 159.037C64.2332 161.355 60.2968 163.037 55.8509 164.083C51.4051 165.128 46.8203 165.651 42.0966 165.651C38.4844 165.651 34.7795 165.378 30.982 164.833C27.1845 164.287 23.4796 163.492 19.8674 162.446C16.2551 161.401 12.7587 160.15 9.37796 158.696C5.99726 157.241 2.87128 155.559 0 153.65L10.0032 134.148L10.1812 134.338C10.6877 134.825 11.8324 135.625 13.6154 136.739C15.6531 138.012 18.177 139.285 21.1872 140.558C24.1974 141.83 27.555 142.967 31.2599 143.967C34.9647 144.967 38.7159 145.467 42.5134 145.467C50.5715 145.467 54.6006 143.058 54.6006 138.239C54.6006 136.421 53.9985 134.921 52.7944 133.739C51.5903 132.557 49.9231 131.489 47.7928 130.534C45.6625 129.579 43.1386 128.693 40.221 127.875C37.3034 127.056 34.1311 126.147 30.7041 125.147C26.1657 123.783 22.2292 122.306 18.8948 120.715C15.5605 119.124 12.805 117.283 10.6284 115.192C8.45174 113.1 6.83086 110.691 5.76571 107.964C4.70056 105.236 4.16798 102.054 4.16798 98.4171C4.16798 93.3257 5.14051 88.8253 7.08557 84.9158C9.03063 81.0063 11.6703 77.7106 15.0047 75.0285C18.3391 72.3464 22.2061 70.3235 26.6056 68.9597C31.0051 67.5959 35.6594 66.9141 40.5683 66.9141ZM131.47 67.7323V164.56H108.685V67.7323H131.47ZM191.39 67.7323L216.954 118.328L242.657 67.7323H267.248V164.56H244.463V106.6L223.067 148.74H210.841L189.445 106.6V164.56H166.66V67.7323H191.39ZM419.925 67.7323V144.922H467.718V164.56H397.14V67.7323H419.925ZM560.072 67.7323V87.3706H514.78V106.191H553.681V124.329H514.78V144.922H561.323V164.56H491.995V67.7323H560.072ZM341.818 67.7323L343.378 67.7677C347.5 67.9563 351.31 68.8991 354.809 70.5962C358.745 72.5055 362.126 75.0058 364.951 78.097C367.776 81.1882 369.999 84.6885 371.62 88.598C373.24 92.5075 374.051 96.4624 374.051 100.463C374.051 104.736 373.287 108.827 371.758 112.737C370.23 116.646 368.1 120.147 365.368 123.238C362.635 126.329 359.324 128.784 355.434 130.602C351.976 132.218 348.189 133.116 344.072 133.296L342.513 133.33H322.507V164.56H299.722V67.7323H341.818ZM340.29 87.3706H322.507V113.828H341.124C343.81 113.828 346.125 112.691 348.07 110.418C350.016 108.145 350.988 104.827 350.988 100.463C350.988 98.1898 350.687 96.2351 350.085 94.5986C349.483 92.9621 348.672 91.5983 347.654 90.5073C346.635 89.4163 345.477 88.6207 344.18 88.1207C342.884 87.6206 341.587 87.3706 340.29 87.3706Z" fill="#062D56"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M642.628 136.08L680.309 173.782L699.513 154.567L699.506 154.561L737.917 116.134L700.236 78.4367L700.243 78.4334L681.404 59.5826L642.993 98.014L642.99 98.0104L681.401 59.5829L643.725 21.881L662.929 2.6652L700.605 40.3673L739.016 1.93511L757.855 20.7859L719.443 59.2176L757.121 96.918L795.533 58.4875L814.373 77.3382L775.959 115.768L813.643 153.471L794.439 172.687L756.756 134.984L718.348 173.415L756.031 211.119L736.827 230.335L699.144 192.63L660.74 231.065L641.901 212.214L680.306 173.78L642.625 136.083L642.628 136.08Z" fill="#07B4B9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M604.77 59.7651L642.446 97.4664L680.856 59.035L699.696 77.8858L661.285 116.317L698.966 154.019L679.762 173.235L642.081 135.532L603.675 173.965L584.836 155.114L623.243 116.682L585.566 78.9809L604.77 59.7651Z" fill="#062D56"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="815" height="233" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="18" viewBox="0 0 24 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12H15" stroke="#0D1333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6H21" stroke="#0D1333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
<defs>
<clipPath id="clip-path">
<rect id="Rectangle_2" data-name="Rectangle 2" width="2971" height="843" transform="translate(0 0.292)" fill="#fff"/>
</clipPath>
<clipPath id="clip-simpleX">
<rect width="1024" height="1024"/>
</clipPath>
</defs>
<g id="simpleX" clip-path="url(#clip-simpleX)">
<g id="logo" transform="translate(-2047 89.708)" clip-path="url(#clip-path)">
<path id="Path_6" data-name="Path 6" d="M147.9,66.914a198.176,198.176,0,0,1,36.975,3.455,284.4,284.4,0,0,1,34.949,8.885,253.446,253.446,0,0,1,31.15,12.34q14.436,6.911,26.592,13.821L241.1,172.542l-.5-.548q-2.161-2.105-10.138-6.856a202.569,202.569,0,0,0-22.793-11.353,224.542,224.542,0,0,0-29.884-10.365,122.159,122.159,0,0,0-32.417-4.442q-44.573,0-44.573,29.122,0,8.885,4.812,14.808t14.182,10.613a133.233,133.233,0,0,0,23.553,8.636q14.182,3.952,32.923,8.885a437.841,437.841,0,0,1,46.6,15.056q20.767,8.143,35.2,20.236A82.161,82.161,0,0,1,280.355,275.7q7.851,17.275,7.851,41.459,0,29.615-11.4,50.1a94.466,94.466,0,0,1-30.137,33.073A129.137,129.137,0,0,1,203.618,418.6a219.939,219.939,0,0,1-50.145,5.675,287.161,287.161,0,0,1-40.521-2.961,299.792,299.792,0,0,1-40.521-8.639A327.515,327.515,0,0,1,34.19,399.1,215.84,215.84,0,0,1,0,380.839l36.469-70.584.649.688q2.77,2.644,12.52,8.69a199.248,199.248,0,0,0,27.6,13.822,287.382,287.382,0,0,0,36.722,12.338,157.851,157.851,0,0,0,41.027,5.429q44.067,0,44.067-26.16a21.773,21.773,0,0,0-6.585-16.287q-6.585-6.417-18.235-11.6a201.006,201.006,0,0,0-27.6-9.624q-15.955-4.446-34.7-9.874a304.855,304.855,0,0,1-43.054-16.041Q50.651,253,38.748,241.647a72.548,72.548,0,0,1-17.728-26.16Q15.2,200.676,15.2,180.933q0-27.641,10.637-48.865A105.947,105.947,0,0,1,54.7,96.283,126.757,126.757,0,0,1,97,74.318,172.123,172.123,0,0,1,147.9,66.914Zm331.4,2.961v350.45H396.237V69.875Zm218.452,0L790.957,253,884.663,69.875h89.652v350.45H891.247V210.55l-78,152.518H768.671l-78-152.518V420.325H607.6V69.875Zm833.178,0V349.249h174.241v71.076H1447.868V69.875Zm510.939,0v71.077H1876.752V209.07h141.823v65.647H1876.752v74.533h169.683v71.076H1793.684V69.875Zm-795.7,0,5.687.128a106.112,106.112,0,0,1,41.674,10.237,121.073,121.073,0,0,1,36.975,27.148,129.172,129.172,0,0,1,24.313,38.006,110.611,110.611,0,0,1,8.863,42.943,120.122,120.122,0,0,1-8.36,44.423,126.219,126.219,0,0,1-23.3,38.006,109.262,109.262,0,0,1-77.64,36.4l-5.684.123h-72.937V420.325h-83.068V69.875Zm-5.571,71.077h-64.832V236.71h67.873q14.689,0,25.323-12.342t10.638-36.03q0-12.341-3.292-21.225a43.245,43.245,0,0,0-8.863-14.808,33.932,33.932,0,0,0-12.665-8.638A39.4,39.4,0,0,0,1240.607,140.953Z" transform="translate(0 175.261)" fill="#062d56" fill-rule="evenodd"/>
<path id="Path_7" data-name="Path 7" d="M644.551,487.448,781.926,623.9l70.013-69.545-.026-.022L991.95,415.257,854.575,278.818l.025-.012-68.682-68.227-140.036,139.1-.011-.013L785.908,210.581,648.551,74.126,718.563,4.578,855.92,141.033,995.957,1.935l68.682,68.227L924.6,209.258l137.364,136.45L1202,206.616l68.685,68.227L1130.641,413.932l137.386,136.459-70.013,69.549L1060.632,483.481,920.607,622.575l137.382,136.463-70.013,69.549L850.594,692.12,710.583,831.228,641.9,763,781.916,623.9,644.541,487.458Z" transform="translate(1698.3 5.061)" fill="#07b4b9" fill-rule="evenodd"/>
<path id="Path_8" data-name="Path 8" d="M657.51,61.677,794.867,198.13,934.9,59.035l68.686,68.227L863.549,266.357l137.375,136.455-70.013,69.549L793.536,335.9,653.518,475l-68.682-68.228,140.022-139.1L587.5,131.225Z" transform="translate(1547.321 154.623)" fill="#062d56" fill-rule="evenodd"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

@@ -1 +0,0 @@
{"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"]}
@@ -1 +0,0 @@
[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/cupertino_icons/CupertinoIcons","fonts":[{"asset":"packages/cupertino_icons/assets/CupertinoIcons.ttf"}]}]
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+41
View File
@@ -0,0 +1,41 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
+91
View File
@@ -0,0 +1,91 @@
PODS:
- DKImagePickerController/Core (4.3.2):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.2)
- DKImagePickerController/PhotoGallery (4.3.2):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.2)
- DKPhotoGallery (0.0.17):
- DKPhotoGallery/Core (= 0.0.17)
- DKPhotoGallery/Model (= 0.0.17)
- DKPhotoGallery/Preview (= 0.0.17)
- DKPhotoGallery/Resource (= 0.0.17)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.17):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.17):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- image_picker (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11)
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- SDWebImage (5.12.0):
- SDWebImage/Core (= 5.12.0)
- SDWebImage/Core (5.12.0)
- share (0.0.1):
- Flutter
- SwiftyGif (5.4.0)
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- image_picker (from `.symlinks/plugins/image_picker/ios`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- share (from `.symlinks/plugins/share/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- MTBBarcodeScanner
- SDWebImage
- SwiftyGif
EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
image_picker:
:path: ".symlinks/plugins/image_picker/ios"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
share:
:path: ".symlinks/plugins/share/ios"
SPEC CHECKSUMS:
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
image_picker: 50e7c7ff960e5f58faa4d1f4af84a771c671bc4a
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
SDWebImage: 4ea20cca2986adc5aacde07aa686742fd4c67a37
share: 0b2c3e82132f5888bccca3351c504d0003b3b410
SwiftyGif: 5d4af95df24caf1c570dbbcb32a3b8a0763bc6d7
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
COCOAPODS: 1.11.0
@@ -13,6 +13,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
AE3860269BF424274DA84C73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B0B8C697FBAC21A75BCBB5A /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -29,12 +30,16 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
031FDA72D548A2F92732ADA4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2FD15BE67425B26EB06BB4AE /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4B0B8C697FBAC21A75BCBB5A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7E09253D517C54DFA8ECEE7A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -49,12 +54,32 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AE3860269BF424274DA84C73 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
415DD71D33C5D1C1BBA9E62B /* Frameworks */ = {
isa = PBXGroup;
children = (
4B0B8C697FBAC21A75BCBB5A /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
5A0F33679D6B606E9775E794 /* Pods */ = {
isa = PBXGroup;
children = (
7E09253D517C54DFA8ECEE7A /* Pods-Runner.debug.xcconfig */,
2FD15BE67425B26EB06BB4AE /* Pods-Runner.release.xcconfig */,
031FDA72D548A2F92732ADA4 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -72,6 +97,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
5A0F33679D6B606E9775E794 /* Pods */,
415DD71D33C5D1C1BBA9E62B /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -105,12 +132,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
AA6465993557C0297F2D0450 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
B4CC1E87FA75B139B238C706 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -197,6 +226,45 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
AA6465993557C0297F2D0450 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
B4CC1E87FA75B139B238C706 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -291,7 +359,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_BUNDLE_IDENTIFIER = com.example.simplexChat;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -415,7 +483,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_BUNDLE_IDENTIFIER = com.example.simplexChat;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -434,7 +502,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_BUNDLE_IDENTIFIER = com.example.simplexChat;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -468,4 +536,4 @@
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
}
@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@@ -0,0 +1,78 @@
import 'dart:async';
import 'package:flutter/material.dart';
class Animator extends StatefulWidget {
final Widget child;
final Duration time;
const Animator(this.child, this.time, {Key? key}) : super(key: key);
@override
_AnimatorState createState() => _AnimatorState();
}
class _AnimatorState extends State<Animator>
with SingleTickerProviderStateMixin {
Timer? timer;
AnimationController? animationController;
Animation? animation;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: const Duration(milliseconds: 290), vsync: this);
animation =
CurvedAnimation(parent: animationController!, curve: Curves.easeInOut);
timer = Timer(widget.time, animationController!.forward);
}
@override
void dispose() {
animationController!.dispose();
super.dispose();
timer!.cancel();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation!,
child: widget.child,
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: animation!.value,
child: Transform.translate(
offset: Offset(0.0, (1 - animation!.value) * 20),
child: child,
),
);
},
);
}
}
Timer? timer;
Duration duration = const Duration();
Duration wait() {
if (timer == null || !timer!.isActive) {
timer = Timer(const Duration(microseconds: 120), () {
duration = const Duration();
});
}
duration += const Duration(milliseconds: 100);
return duration;
}
class WidgetAnimator extends StatelessWidget {
final Widget child;
const WidgetAnimator({required this.child, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Animator(child, wait());
}
}
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
class EntranceFader extends StatefulWidget {
/// Child to be animated on entrance
final Widget child;
/// Delay after which the animation will start
final Duration delay;
/// Duration of entrance animation
final Duration duration;
/// Starting point from which the widget will fade to its default position
final Offset offset;
const EntranceFader({
Key? key,
required this.child,
this.delay = const Duration(milliseconds: 0),
this.duration = const Duration(milliseconds: 400),
this.offset = const Offset(0.0, 32.0),
}) : super(key: key);
@override
EntranceFaderState createState() {
return EntranceFaderState();
}
}
class EntranceFaderState extends State<EntranceFader>
with SingleTickerProviderStateMixin {
AnimationController? _controller;
Animation? _dxAnimation;
Animation? _dyAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
_dxAnimation =
Tween(begin: widget.offset.dx, end: 0.0).animate(_controller!);
_dyAnimation =
Tween(begin: widget.offset.dy, end: 0.0).animate(_controller!);
Future.delayed(widget.delay, () {
if (mounted) {
_controller!.forward();
}
});
}
@override
void dispose() {
_controller!.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller!,
builder: (context, child) => Opacity(
opacity: _controller!.value,
child: Transform.translate(
offset: Offset(_dxAnimation!.value, _dyAnimation!.value),
child: widget.child,
),
),
);
}
}
+12
View File
@@ -0,0 +1,12 @@
/// All the named routes are mentioned here
class AppRoutes {
static const splash = '/splash';
static const intro = '/intro';
static const setupProfile = '/setupProfile';
static const home = '/home';
static const addContact = '/addContact';
static const addContactWeb = '/addContactWeb';
static const scanInvitation = '/ScanInvitation';
static const addGroup = '/addGroup';
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
// colors
const kPrimaryColor = Color(0xff062d56);
const kSecondaryColor = Color(0xff07b4b9);
// text styles
const TextStyle kHeadingStyle = TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.bold,
letterSpacing: 1.3,
color: kPrimaryColor,
);
const TextStyle kMediumHeadingStyle = TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w500,
color: kPrimaryColor,
);
const TextStyle kSmallHeadingStyle = TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
);
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class ScrollBehaviorModified extends ScrollBehavior {
const ScrollBehaviorModified();
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
case TargetPlatform.android:
return const BouncingScrollPhysics();
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return const ClampingScrollPhysics();
}
}
}
@@ -0,0 +1,20 @@
//
// Generated file. Do not edit.
//
// ignore_for_file: directives_ordering
// ignore_for_file: lines_longer_than_80_chars
import 'package:file_picker/_internal/file_picker_web.dart';
import 'package:image_picker_for_web/image_picker_for_web.dart';
import 'package:shared_preferences_web/shared_preferences_web.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
// ignore: public_member_api_docs
void registerPlugins(Registrar registrar) {
FilePickerWeb.registerWith(registrar);
ImagePickerPlugin.registerWith(registrar);
SharedPreferencesPlugin.registerWith(registrar);
registrar.registerMessageHandler();
}
+51 -99
View File
@@ -1,115 +1,67 @@
import "package:flutter/material.dart";
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:simplex_chat/app_routes.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/custom_scroll_behavior.dart';
import 'package:simplex_chat/providers/drawer_providers.dart';
import 'package:simplex_chat/views/contacts/add_contact_view.dart';
import 'package:simplex_chat/views/contacts/add_contact_web.dart';
import 'package:simplex_chat/views/group/add_group_view.dart';
import 'package:simplex_chat/views/home/home_view.dart';
import 'package:simplex_chat/views/onboarding/intro_view.dart';
import 'package:simplex_chat/views/scan_invitation/scan_invitation_view.dart';
import 'package:simplex_chat/views/setup_profile_view.dart';
import 'package:simplex_chat/views/splash_screen.dart';
import 'package:url_strategy/url_strategy.dart';
/// Basic Structure is setup on [Providers]
/// Navigations are [namedRoutes]
void main() {
WidgetsFlutterBinding.ensureInitialized();
setPathUrlStrategy();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter Demo",
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: "Flutter Demo Home Page"),
final theme = ThemeData(
primarySwatch: Colors.teal,
primaryColor: kPrimaryColor,
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
"You have pushed the button this many times:",
),
Text(
"$_counter",
style: Theme.of(context).textTheme.headline4,
),
],
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => DrawerProvider()),
],
child: Consumer<DrawerProvider>(
builder: (context, drawerProvider, chidl) => MaterialApp(
debugShowCheckedModeBanner: false,
title: 'SimpleX Chat',
theme: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(secondary: kPrimaryColor),
primaryColorLight: Colors.white,
),
builder: (context, widget) {
return ScrollConfiguration(
behavior: const ScrollBehaviorModified(), child: widget!);
},
initialRoute: AppRoutes.splash,
routes: <String, WidgetBuilder>{
AppRoutes.splash: (_) => const SplashScreen(),
AppRoutes.intro: (_) => const IntroView(),
AppRoutes.setupProfile: (_) => const SetupProfileView(),
AppRoutes.home: (_) => const HomeView(),
AppRoutes.addContact: (_) => const AddContactView(),
AppRoutes.addContactWeb: (_) => const AddContactWeb(),
AppRoutes.scanInvitation: (_) => const ScanInvitationView(),
AppRoutes.addGroup: (_) => const AddGroupView(),
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: "Increment",
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
@@ -0,0 +1,44 @@
import 'dart:convert';
class Contact {
final String? name;
final String? subtitle;
final String? photo;
final bool? isGroup;
Contact({
required this.name,
required this.subtitle,
this.photo = '',
this.isGroup = false,
});
factory Contact.fromJson(Map<String, dynamic> json) {
return Contact(
name: json['name'],
subtitle: json['subtitle'],
photo: json['photo'],
isGroup: json['isGroup'],
);
}
static Map<String, dynamic> toJson(Contact contact) {
return {
'name': contact.name,
'subtitle': contact.subtitle,
'photo': contact.photo,
'isGroup': false,
};
}
static String encode(List<Contact> contacts) => json.encode(
contacts
.map<Map<String, dynamic>>((contact) => Contact.toJson(contact))
.toList(),
);
static List<Contact> decode(String contacts) =>
(json.decode(contacts) as List<dynamic>)
.map<Contact>((item) => Contact.fromJson(item))
.toList();
}
+48
View File
@@ -0,0 +1,48 @@
import 'dart:convert';
class Group {
final String? name;
final String? subtitle;
final String? photo;
final List<dynamic>? members;
final bool? isGroup;
Group({
required this.name,
required this.subtitle,
this.photo = '',
this.isGroup = true,
this.members = const [],
});
factory Group.fromJson(Map<String, dynamic> json) {
return Group(
name: json['name'],
subtitle: json['subtitle'],
photo: json['photo'],
isGroup: json['isGroup'],
members: json['members'],
);
}
static Map<String, dynamic> toJson(Group group) {
return {
'name': group.name,
'subtitle': group.subtitle,
'photo': group.photo,
'isGroup': true,
'members': group.members,
};
}
static String encode(List<Group> groups) => json.encode(
groups
.map<Map<String, dynamic>>((group) => Group.toJson(group))
.toList(),
);
static List<Group> decode(String groups) =>
(json.decode(groups) as List<dynamic>)
.map<Group>((item) => Group.fromJson(item))
.toList();
}
@@ -0,0 +1,12 @@
import 'package:flutter/cupertino.dart';
class DrawerProvider extends ChangeNotifier {
int _currentIndex = 1;
int get currentIndex => _currentIndex;
set currentIndex(int value) {
_currentIndex = value;
notifyListeners();
}
}
@@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class AddContactView extends StatefulWidget {
const AddContactView({Key? key}) : super(key: key);
@override
_AddContactViewState createState() => _AddContactViewState();
}
class _AddContactViewState extends State<AddContactView> {
bool? _dontShow = false;
final qrKey = GlobalKey(debugLabel: 'qr');
QRViewController? _qrViewController;
Barcode? result;
// alert dialgoue
void _initialWarning() {
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Are you Sure?'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Your profile will be sent to your contact!'),
const SizedBox(height: 15.0),
Row(
children: [
Checkbox(
value: _dontShow,
onChanged: (value) {
setState(() {
_dontShow = value;
});
}),
const Text("Don't ask again")
],
),
],
),
actions: [
InkWell(
onTap: () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.check, color: Colors.green),
),
),
InkWell(
onTap: () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.cancel_outlined, color: Colors.red),
),
)
],
),
),
);
}
void _intiBox() {
Future.delayed(const Duration(seconds: 1), _initialWarning);
}
@override
void initState() {
_intiBox();
super.initState();
}
@override
void dispose() {
_qrViewController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Add Contact'),
actions: [
IconButton(
icon: FutureBuilder(
future: _qrViewController?.getFlashStatus(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Icon(snapshot.data == false
? Icons.flash_on
: Icons.flash_off);
}
return const Icon(Icons.flash_off);
},
),
onPressed: () async {
await _qrViewController?.toggleFlash();
setState(() {});
},
),
IconButton(
icon: FutureBuilder(
future: _qrViewController?.getCameraInfo(),
builder: (context, snapshot) {
return const Icon(Icons.camera_alt);
},
),
onPressed: () async {
await _qrViewController?.flipCamera();
setState(() {});
},
),
],
),
body: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
Center(
child: GestureDetector(
onTap: () => Navigator.of(context).pop(true),
child: _qrViewBuild()),
),
const Center(
child: Text(
'Tap Here!',
style:
TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
Positioned(
top: MediaQuery.of(context).size.height * 0.15,
left: MediaQuery.of(context).size.width * 0.125,
child: const Text(
'Position QR Code within the frame.',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 18.0,
),
),
),
],
),
),
);
}
QRView _qrViewBuild() {
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
? 220.0
: 370.0;
return QRView(
key: qrKey,
onQRViewCreated: (QRViewController controller) {
_qrViewController = controller;
controller.scannedDataStream.listen((scanData) {
setState(() async {
result = scanData;
await controller.stopCamera();
if (result != null) {
Navigator.of(context).pop(true);
}
});
});
},
overlay: QrScannerOverlayShape(
borderColor: Colors.red,
borderRadius: 0,
borderLength: 50,
borderWidth: 10,
cutOutSize: scanArea,
),
);
}
}
@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class AddContactWeb extends StatefulWidget {
const AddContactWeb({Key? key}) : super(key: key);
@override
_AddContactWebState createState() => _AddContactWebState();
}
class _AddContactWebState extends State<AddContactWeb> {
bool? _dontShow = false;
// alert dialgoue
void _initialWarning() {
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Are you Sure?'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Your profile will be sent to your contact!'),
const SizedBox(height: 15.0),
Row(
children: [
Checkbox(
value: _dontShow,
onChanged: (value) {
setState(() {
_dontShow = value;
});
}),
const Text("Don't ask again")
],
),
],
),
actions: [
InkWell(
onTap: () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.check, color: Colors.green),
),
),
InkWell(
onTap: () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.cancel_outlined, color: Colors.red),
),
)
],
),
),
);
}
void _intiBox() {
Future.delayed(const Duration(seconds: 1), _initialWarning);
}
@override
void initState() {
_intiBox();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('Add Contact'),
),
body: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
Center(
child: GestureDetector(
onTap: () => Navigator.of(context).pop(true),
child: Container(
padding: const EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.red,
width: 5.0,
),
),
child: Image.asset('assets/code.png')),
),
),
Positioned(
top: MediaQuery.of(context).size.height * 0.15,
left: MediaQuery.of(context).size.width * 0.41,
child: const Text(
'Position QR Code within the frame.',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 18.0,
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,482 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplex_chat/animations/bottom_animation.dart';
import 'package:simplex_chat/app_routes.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/model/contact.dart';
import 'package:simplex_chat/model/group.dart';
import 'package:simplex_chat/views/conversation/conversation_view.dart';
class Conversations extends StatefulWidget {
const Conversations({Key? key}) : super(key: key);
@override
_ConversationsState createState() => _ConversationsState();
}
class _ConversationsState extends State<Conversations> {
bool? _eraseMedia = false;
List<dynamic> _conversations = [];
List<Contact> _contactsList = []; // for storing contacts
List<Group> _groupList = []; // for storing groups
final List<String> _options = [
'Add contact',
'Scan invitation',
'Add group',
];
// getting data from local storage FOR NOW!!
void _getContacts() async {
debugPrint('Getting contacts!');
SharedPreferences prefs = await SharedPreferences.getInstance();
final String? _contacts = prefs.getString('contacts');
if (_contacts != null) {
setState(() {
_contactsList = List.from(Contact.decode(_contacts));
});
}
}
// getting data from local storage FOR NOW!!
void _getGroups() async {
debugPrint('Getting groups!');
SharedPreferences prefs = await SharedPreferences.getInstance();
final String? _groups = prefs.getString('groups');
if (_groups != null) {
setState(() {
_groupList = List.from(Group.decode(_groups));
});
}
_gettingGroupContactsChats();
}
void _gettingGroupContactsChats() {
debugPrint('Merging!');
setState(() {
_conversations = List.from(_contactsList);
_conversations = _conversations + _groupList;
});
}
@override
void initState() {
_getContacts();
_getGroups();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.transparent,
leadingWidth: 30,
title: InkWell(
onTap: _addNewContacts,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.bug_report,
color: Colors.grey,
size: 22.0,
),
),
),
),
backgroundColor: Colors.white,
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
child: Center(
child: Column(
children: [
const SizedBox(height: 10.0),
_contactsList.isEmpty
? SizedBox(
height: MediaQuery.of(context).size.height * 0.7,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"You don't have any conversation yet!",
style: kMediumHeadingStyle,
textAlign: TextAlign.center,
),
SizedBox(height: 8.0),
Text(
'Click the icon below to add a contact',
textAlign: TextAlign.center,
),
],
),
),
)
: ListView(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
children: List.generate(
_conversations.length,
(index) => WidgetAnimator(
child: ListTile(
leading: CircleAvatar(
backgroundImage:
// ignore: avoid_dynamic_calls
_conversations[index].photo == ''
? const AssetImage('assets/dp.png')
as ImageProvider
: FileImage(
// ignore: avoid_dynamic_calls
File(_conversations[index].photo ??
'')),
),
// ignore: avoid_dynamic_calls
title: Text(_conversations[index].name ?? ''),
subtitle:
// ignore: avoid_dynamic_calls
Text(_conversations[index].subtitle ?? ''),
// ignore: avoid_dynamic_calls
trailing: Icon(
// ignore: avoid_dynamic_calls
_conversations[index].isGroup
? Icons.group
: Icons.person,
size: 18,
color: Colors.grey[400],
),
onTap: () async {
var value = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ConversationView(
data: _conversations[index],
),
),
);
value ??= false;
if (value) {
_getContacts();
_getGroups();
}
},
onLongPress: () =>
_conversationOptions(_conversations[index]),
),
),
),
),
],
),
),
),
),
floatingActionButton: PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0),
),
offset: const Offset(-10, -155),
onSelected: (value) async {
if (value == _options[0]) {
var newMember =
await Navigator.pushNamed(context, AppRoutes.scanInvitation);
newMember ??= false;
if (newMember == true) {
_addNewMember();
}
} else if (value == _options[1]) {
var newMember = await Navigator.pushNamed(context,
kIsWeb ? AppRoutes.addContactWeb : AppRoutes.addContact);
newMember ??= false;
if (newMember == true) {
_addNewMember();
}
} else {
var newGroup =
await Navigator.pushNamed(context, AppRoutes.addGroup);
if (newGroup == true) {
_getGroups();
}
}
},
itemBuilder: (context) => _options
.map(
(opt) => PopupMenuItem(
value: opt,
child: Text(opt),
),
)
.toList(),
child: const FloatingActionButton(
heroTag: 'connect',
onPressed: null,
child: Icon(
Icons.person_add,
),
),
),
);
}
// delete a contact
void _deleteContact(Contact contact) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_contactsList.remove(contact);
});
await prefs.setString('contacts', Contact.encode(_contactsList));
var snackBar = SnackBar(
backgroundColor: Colors.red,
content: Text('${contact.name} deleted!'),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
}
// delete a group
void _deleteGroup(Group group) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_groupList.remove(group);
});
await prefs.setString('groups', Group.encode(_groupList));
var snackBar = SnackBar(
backgroundColor: Colors.red,
content: Text('${group.name} deleted!'),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
}
void _conversationOptions(var chat) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
_deleteConversation(chat);
},
child: const Text(
'Delete Conversation',
style: TextStyle(color: Colors.red),
)),
TextButton(
onPressed: () {
Navigator.pop(context);
_disconnect();
},
child: const Text('Disconnect')),
],
),
),
);
}
void _deleteConversation(var chat) {
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Are you Sure?'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'All conversation history will be deleted from your device!'),
const SizedBox(height: 15.0),
Row(
children: [
Checkbox(
value: _eraseMedia,
onChanged: (value) {
setState(() {
_eraseMedia = value;
});
}),
const Text('Erase files & Media')
],
),
],
),
actions: [
InkWell(
// ignore: avoid_dynamic_calls
onTap: chat.isGroup
? () {
_deleteGroup(chat);
_conversations.remove(chat);
Navigator.pop(context);
}
: () {
_deleteContact(chat);
_conversations.remove(chat);
Navigator.pop(context);
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.check, color: Colors.green),
),
),
InkWell(
onTap: () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.cancel_outlined, color: Colors.red),
),
)
],
),
),
);
}
void _disconnect() {
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Are you Sure?'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Disconnecting will erase all the data from your device and you will no longer be able to contact again!'),
const SizedBox(height: 15.0),
Row(
children: [
Checkbox(
value: _eraseMedia,
onChanged: (value) {
setState(() {
_eraseMedia = value;
});
}),
const Text('Erase files & Media')
],
),
],
),
actions: [
InkWell(
onTap: () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.check, color: Colors.green),
),
),
InkWell(
onTap: () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.cancel_outlined, color: Colors.red),
),
)
],
),
),
);
}
// dummy ftn for loading new contacts
void _addNewContacts() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
// adding dummy contact
List<Contact> _localList = [];
final String? _local = prefs.getString('contacts');
if (_local != null) {
_localList = List.from(Contact.decode(_local));
}
List<Contact> _newList = [
Contact(
name: 'Harry',
subtitle: 'Hello!',
),
];
_newList = _localList + _newList;
// dummy ftn for filling the list
final String _newContacts = Contact.encode(_newList);
await prefs.setString('contacts', _newContacts);
// adding dummy contact
List<Group> _localListGroup = [];
final String? _localGroups = prefs.getString('groups');
if (_local != null) {
_localListGroup = List.from(Group.decode(_localGroups!));
}
List<Group> _newGroups = [
Group(
name: 'College Friends',
subtitle: 'Lovely people',
members: ['Alice', 'James', 'Rio']),
];
_newGroups = _localListGroup + _newGroups;
// dummy ftn for filling the list
final String _newGroup = Group.encode(_newGroups);
await prefs.setString('groups', _newGroup);
_getContacts();
_getGroups();
const snackBar = SnackBar(
backgroundColor: Colors.green,
content: Text('New contacts loaded!'),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
}
void _addNewMember() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
// adding dummy contact
List<Contact> _localList = [];
final String? _local = prefs.getString('contacts');
if (_local != null) {
_localList = List.from(Contact.decode(_local));
}
List<Contact> _newList = [
Contact(
name: 'Bob',
subtitle: 'You and Bob are connected now!',
),
];
_newList = _localList + _newList;
// dummy ftn for filling the list
final String _newContacts = Contact.encode(_newList);
await prefs.setString('contacts', _newContacts);
_getContacts();
_getGroups();
const snackBar = SnackBar(
backgroundColor: Colors.green,
content: Text('New connection added!'),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
}
}
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/model/contact.dart';
class ContactDetailsConversation extends StatelessWidget {
final Contact contact;
const ContactDetailsConversation({Key? key, required this.contact})
: super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(contact.name!),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircleAvatar(
radius: 65,
backgroundImage: AssetImage('assets/dp.png'),
),
const SizedBox(height: 15.0),
const Text('Display Name', style: kMediumHeadingStyle),
Text(contact.name!),
],
),
),
),
);
}
}
@@ -0,0 +1,211 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:simplex_chat/views/conversation/contact_detail_conversation.dart';
import 'package:simplex_chat/views/conversation/group_detail_conversation.dart';
import 'package:simplex_chat/widgets/message_bubble.dart';
/// View the details of group by tapping the [appBar]
/// Viwe the details of chat by tapping the [appBar]
/// Generate [Fake Messages] by tapping the [Message Icon]
class ConversationView extends StatefulWidget {
// ignore: prefer_typing_uninitialized_variables
final data;
const ConversationView({Key? key, this.data}) : super(key: key);
@override
_ConversationViewState createState() => _ConversationViewState();
}
class _ConversationViewState extends State<ConversationView> {
final ScrollController _scrollController = ScrollController();
final TextEditingController _messageFieldController = TextEditingController();
FocusNode? _focus;
bool _fieldEnabled = false;
final List<Widget> _chatMessages = [];
@override
void initState() {
_focus = FocusNode();
_focus!.addListener(() {
debugPrint('FOCUS ${_focus!.hasFocus}');
_fieldEnabled = _focus!.hasFocus;
debugPrint('MESSAGE ENABLED $_fieldEnabled');
});
super.initState();
}
@override
void dispose() {
_messageFieldController.dispose();
_scrollController.dispose();
_focus!.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () => Navigator.of(context).pop(true),
),
leadingWidth: MediaQuery.of(context).size.width * 0.085,
title: InkWell(
// ignore: avoid_dynamic_calls
onTap: widget.data.isGroup
? () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GroupDetailsConversation(
group: widget.data,
),
),
);
}
: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ContactDetailsConversation(
contact: widget.data,
),
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
CircleAvatar(
radius: 15,
// ignore: avoid_dynamic_calls
backgroundImage: widget.data.photo == ''
? const AssetImage('assets/dp.png') as ImageProvider
: FileImage(
// ignore: avoid_dynamic_calls
File(widget.data.photo),
),
),
const SizedBox(width: 10.0),
Text(
// ignore: avoid_dynamic_calls
widget.data.name,
),
],
),
),
),
actions: [
IconButton(
onPressed: () {
setState(() {
_chatMessages.add(MessageBubble(
isUser: false,
// ignore: avoid_dynamic_calls
sender: widget.data.isGroup
// ignore: avoid_dynamic_calls
? widget.data.members.length == 0
? 'some person'
// ignore: avoid_dynamic_calls
: widget.data.members[0]
// ignore: avoid_dynamic_calls
: widget.data.name,
text: 'Hey there! How is it going?',
));
});
},
icon: const Icon(
Icons.bug_report,
),
)
],
),
body: Column(
children: [
Expanded(
child: Container(
child: _chatMessages.isEmpty
? const Center(
child: Text('Send a message to get started'),
)
: SingleChildScrollView(
controller: _scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _chatMessages,
),
),
),
),
Container(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 10.0),
Expanded(
child: SizedBox(
height: 45.0,
child: TextFormField(
maxLines: null,
focusNode: _focus,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
controller: _messageFieldController,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
left: 10.0,
),
hintText: 'Message...',
hintStyle: Theme.of(context).textTheme.caption,
fillColor: Colors.grey[200],
filled: true,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(360),
borderSide:
const BorderSide(color: Colors.transparent),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(360),
borderSide:
const BorderSide(color: Colors.transparent),
),
),
),
)),
IconButton(
onPressed: () {
if (_messageFieldController.text.isNotEmpty) {
setState(() {
_chatMessages.add(MessageBubble(
isUser: true,
sender: 'You',
text: _messageFieldController.text.trim(),
));
});
_messageFieldController.clear();
_focus!.unfocus();
}
},
icon: const Icon(Icons.send_rounded,
size: 25.0, color: Colors.teal),
),
],
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.02,
)
],
),
),
);
}
}
@@ -0,0 +1,330 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/model/contact.dart';
import 'package:simplex_chat/model/group.dart';
enum MemberSetting { owner, admin, member }
class GroupDetailsConversation extends StatefulWidget {
final Group group;
const GroupDetailsConversation({Key? key, required this.group})
: super(key: key);
@override
_GroupDetailsConversationState createState() =>
_GroupDetailsConversationState();
}
class _GroupDetailsConversationState extends State<GroupDetailsConversation> {
MemberSetting _memberSetting = MemberSetting.member;
bool _addMember = false;
List<Contact> _contactsList = []; // for storing contacts
List _newMembers = [];
List<Group> _groupList = [];
List _members = [];
// getting all members of group
void _getMembers() {
setState(() {
_members = List.from(widget.group.members!);
});
}
// getting groups
void _getGroups() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final String? _groups = prefs.getString('groups');
if (_groups != null) {
setState(() {
_groupList = List.from(Group.decode(_groups));
});
}
}
// getting data from local storage FOR NOW
void _getContacts() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final String? _contacts = prefs.getString('contacts');
setState(() {
_contactsList = List.from(Contact.decode(_contacts!));
});
}
@override
void initState() {
_getContacts();
_getGroups();
_getMembers();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(widget.group.name!),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: CircleAvatar(
radius: 70,
backgroundImage: widget.group.photo == ''
? const AssetImage('assets/dp.png') as ImageProvider
: FileImage(File(widget.group.photo!)),
),
),
const SizedBox(height: 25.0),
const Text('Group Name', style: kMediumHeadingStyle),
Text(widget.group.name!),
const Divider(),
ListTile(
leading: const Icon(Icons.person_add),
title: const Text('Add a member'),
onTap: () {
setState(() {
_addMember = !_addMember;
});
},
),
const Divider(),
_newMembers.isNotEmpty
? const Text('New Members Added')
: Container(),
SizedBox(height: _newMembers.isNotEmpty ? 10.0 : 0.0),
_newMembers.isNotEmpty
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
_newMembers.length,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: InkWell(
onTap: () {
setState(() {
_newMembers.remove(_newMembers[index]);
});
},
child: Container(
padding: const EdgeInsets.all(7.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
color: Colors.grey.withAlpha(100)),
child: Text(_newMembers[index]),
),
),
),
)),
)
: Container(),
SizedBox(height: _addMember ? 10.0 : 0.0),
_addMember ? const Text('Contacts Available') : Container(),
SizedBox(height: _addMember ? 10.0 : 0.0),
_addMember
? ListView(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
children: List.generate(
_contactsList.length,
(index) => ListTile(
leading: const CircleAvatar(
backgroundImage: AssetImage('assets/dp.png'),
),
title: Text(_contactsList[index].name!),
onTap: () {
setState(() {
_newMembers.add(_contactsList[index].name);
});
},
),
),
)
: Container(),
SizedBox(height: _addMember ? 10.0 : 0.0),
const Text('Members', style: kMediumHeadingStyle),
const SizedBox(height: 5.0),
for (int i = 0; i < _members.length; i++)
ListTile(
leading: const CircleAvatar(
backgroundImage: AssetImage('assets/dp.png'),
),
title: Text(_members[i]),
trailing: InkWell(
onTap: () => _memberSettings(_members[i]),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.settings),
),
),
)
],
),
),
),
floatingActionButton: _newMembers.isEmpty
? Container()
: FloatingActionButton(
onPressed: _addNewMembers,
child: const Icon(Icons.check),
),
);
}
void _memberSettings(String contact) {
Size _size = MediaQuery.of(context).size;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(width: _size.width * 0.15),
const Expanded(child: Text('Owner')),
Radio(
value: MemberSetting.owner,
groupValue: _memberSetting,
onChanged: (MemberSetting? value) {
setState(() {
_memberSetting = value!;
});
}),
SizedBox(width: _size.width * 0.15),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(width: _size.width * 0.15),
const Expanded(child: Text('Admin')),
Radio(
groupValue: _memberSetting,
value: MemberSetting.admin,
onChanged: (MemberSetting? value) {
setState(() {
_memberSetting = value!;
});
}),
SizedBox(width: _size.width * 0.15),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(width: _size.width * 0.15),
const Expanded(child: Text('Member')),
Radio(
groupValue: _memberSetting,
value: MemberSetting.member,
onChanged: (MemberSetting? value) {
setState(() {
_memberSetting = value!;
});
}),
SizedBox(width: _size.width * 0.15),
],
),
const Divider(),
TextButton(
onPressed: () => _removeMember(contact),
child: const Text('Remove from group',
style: TextStyle(color: Colors.red)),
),
],
),
),
),
);
}
void _addNewMembers() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
// add new members to the _members list
setState(() {
_members = _members + _newMembers;
_newMembers = [];
_addMember = false;
});
const snackBar = SnackBar(
backgroundColor: Colors.green, content: Text('New members added!'));
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
// get index of group in local
int index = 0;
index = _groupList.indexWhere((group) => group.name == widget.group.name);
// add the full _members list to the group
Group _updatedGroup = Group(
name: widget.group.name,
subtitle: widget.group.subtitle,
members: _members,
photo: widget.group.photo,
);
// put it in updated group local list
_groupList[index] = _updatedGroup;
// convert to string
final String _updatedGroupString = Group.encode(_groupList);
// update the group in local storage
await prefs.setString('groups', _updatedGroupString);
}
void _removeMember(String contact) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
Navigator.pop(context);
// remove the current member
setState(() {
_members.remove(contact);
});
var snackBar = SnackBar(
backgroundColor: Colors.red, content: Text('$contact removed!'));
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
// get index of group in local
int index = 0;
index = _groupList.indexWhere((group) => group.name == widget.group.name);
// new instance of group (updated)
Group _updatedGroup = Group(
name: widget.group.name,
subtitle: widget.group.subtitle,
photo: widget.group.photo,
members: _members,
);
// put it in group local list
_groupList[index] = _updatedGroup;
// convert to string
final String _updatedGroupString = Group.encode(_groupList);
// update the group in local storage
await prefs.setString('groups', _updatedGroupString);
}
}
@@ -0,0 +1,432 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/model/contact.dart';
import 'package:simplex_chat/model/group.dart';
import 'package:simplex_chat/widgets/custom_text_field.dart';
class AddGroupView extends StatefulWidget {
const AddGroupView({Key? key}) : super(key: key);
@override
_AddGroupViewState createState() => _AddGroupViewState();
}
class _AddGroupViewState extends State<AddGroupView> {
// Image Picker --> DP properties
final imgPicker = ImagePicker();
File? image;
String _groupPhotoPath = '';
bool _uploading = false;
bool _imageUploaded = false;
final List _members = [];
final _formKey = GlobalKey<FormState>();
final _displayNameController = TextEditingController();
final _descController = TextEditingController();
bool _addMember = false;
List<Contact> _contactsList = []; // for storing contacts
// image buttons options
final _dpBtnText = ['Remove', 'Gallery', 'Camera'];
final _dpBtnColors = [Colors.red, Colors.purple, Colors.green];
final _dpBtnIcons = [
Icons.delete,
Icons.photo_rounded,
Icons.camera_alt_rounded
];
// getting data from local storage FOR NOW
void _getContacts() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final String? _contacts = prefs.getString('contacts');
setState(() {
_contactsList = List.from(Contact.decode(_contacts!));
});
}
String _userPhotoPath = '';
void _getUserPhoto() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? _photo = prefs.getString('photo${prefs.getString('displayName')}');
if (_photo != null) {
setState(() {
_userPhotoPath = _photo;
});
}
debugPrint(_userPhotoPath);
}
@override
void initState() {
_getUserPhoto();
_getContacts();
super.initState();
}
@override
void dispose() {
_displayNameController.dispose();
_descController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Size _size = MediaQuery.of(context).size;
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () => Navigator.of(context).pop(false),
),
title: const Text('New Group'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: _size.height * 0.012),
Center(
child: SizedBox(
height: 180.0,
width: 180.0,
child: Stack(
children: [
_imageUploaded
? CircleAvatar(
radius: _size.height * 0.12,
backgroundImage:
FileImage(File(_groupPhotoPath)),
)
: CircleAvatar(
radius: _size.height * 0.12,
backgroundImage:
const AssetImage('assets/dp.png'),
),
Positioned(
right: 0,
bottom: 0,
child: FloatingActionButton(
backgroundColor: kSecondaryColor,
elevation: 2.0,
mini: true,
onPressed: _updateProfilePic,
child: _uploading
? const SizedBox(
height: 18.0,
width: 18.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: const Icon(
Icons.add_a_photo,
size: 20,
),
),
)
],
),
),
),
SizedBox(height: _size.height * 0.03),
const Text('Group Name', style: kSmallHeadingStyle),
SizedBox(height: _size.height * 0.012),
CustomTextField(
textEditingController: _displayNameController,
textInputType: TextInputType.name,
hintText: 'e.g College friends',
validatorFtn: (String? value) {
if (value!.isEmpty) {
return 'Group name cannot be empty';
}
return null;
},
),
SizedBox(height: _size.height * 0.012),
const Text('Group Description', style: kSmallHeadingStyle),
SizedBox(height: _size.height * 0.012),
CustomTextField(
textEditingController: _descController,
textInputType: TextInputType.text,
hintText: 'e.g Friends from UK',
),
SizedBox(height: _size.height * 0.012),
_members.isNotEmpty
? const Text('Members Added')
: Container(),
SizedBox(height: _members.isNotEmpty ? 10.0 : 0.0),
_members.isNotEmpty
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
_members.length,
(index) => Padding(
padding:
const EdgeInsets.symmetric(horizontal: 5.0),
child: InkWell(
onTap: () {
setState(() {
_members.remove(_members[index]);
});
},
child: Container(
padding: const EdgeInsets.all(7.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
color: Colors.grey.withAlpha(100)),
child: Text(_members[index]),
),
),
),
)),
)
: Container(),
SizedBox(
height: _members.isNotEmpty ? _size.height * 0.012 : 0.0),
ListTile(
leading: const Icon(Icons.person_add),
title: const Text('Add a member'),
onTap: () {
setState(() {
_addMember = !_addMember;
});
},
),
SizedBox(height: _addMember ? _size.height * 0.012 : 0.0),
_addMember ? const Text('Contacts Available') : Container(),
SizedBox(height: _addMember ? _size.height * 0.012 : 0.0),
_addMember
? ListView(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
children: List.generate(
_contactsList.length,
(index) => ListTile(
leading: const CircleAvatar(
backgroundImage: AssetImage('assets/dp.png'),
),
title: Text(_contactsList[index].name!),
onTap: () {
setState(() {
_members.add(_contactsList[index].name);
});
},
),
),
)
: Container(),
Divider(height: _size.height * 0.035),
ListTile(
leading: CircleAvatar(
backgroundImage: _userPhotoPath == ''
? const AssetImage('assets/dp.png') as ImageProvider
: FileImage(File(_userPhotoPath)),
),
title: const Text('You'),
subtitle: const Text(
'Owner',
style: TextStyle(color: Colors.grey, fontSize: 12.0),
)),
],
),
),
),
),
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton(
heroTag: 'setup',
onPressed: () async {
if (_formKey.currentState!.validate()) {
FocusScope.of(context).unfocus();
_addNewGroup(_displayNameController.text.trim(),
_descController.text.trim());
_descController.clear();
_displayNameController.clear();
Navigator.of(context).pop(true);
}
},
child: const Icon(Icons.check),
),
),
),
);
}
void _updateProfilePic() {
showModalBottomSheet(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0),
topRight: Radius.circular(10.0),
),
),
context: context,
builder: (context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(360.0)),
height: 7.0,
width: 50.0,
),
const SizedBox(height: 20.0),
const Align(
alignment: Alignment.centerLeft,
child: Text(
' Profile photo',
style: kHeadingStyle,
),
),
const SizedBox(height: 15.0),
Row(
children: List.generate(
3,
(index) => Column(
mainAxisSize: MainAxisSize.min,
children: [
MaterialButton(
color: _dpBtnColors.map((e) => e).elementAt(index),
shape: const CircleBorder(),
onPressed: index == 0
? () => _removePic()
: index == 1
? () => _galleryPic()
: () => _cameraPic(),
child: Icon(
_dpBtnIcons.map((e) => e).elementAt(index),
color: Colors.white,
),
),
Text(
_dpBtnText.map((e) => e).elementAt(index),
textAlign: TextAlign.center,
)
],
),
))
],
),
),
);
}
void _removePic() {
setState(() {
_imageUploaded = false;
image = null;
_groupPhotoPath = '';
});
Navigator.pop(context);
}
void _cameraPic() async {
try {
setState(() {
_uploading = true;
});
// picking Image from Camera
final file = await imgPicker.getImage(
source: ImageSource.camera,
);
if (file != null) {
image = File(file.path);
setState(() {
_uploading = false;
_imageUploaded = true;
_groupPhotoPath = file.path;
});
} else {
setState(() {
_uploading = false;
});
}
Navigator.pop(context);
} catch (e) {
rethrow;
}
}
void _galleryPic() async {
try {
setState(() {
_uploading = true;
});
// picking Image from local storage
final file = await imgPicker.getImage(
source: ImageSource.gallery,
);
if (file != null) {
image = File(file.path);
setState(() {
_uploading = false;
_imageUploaded = true;
_groupPhotoPath = file.path;
});
} else {
setState(() {
_uploading = false;
});
}
Navigator.pop(context);
} catch (e) {
rethrow;
}
}
void _addNewGroup(String name, String desc) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<Group> _localList = [];
final String? _local = prefs.getString('groups');
if (_local != null) {
_localList = List.from(Group.decode(_local));
}
List<Group> _groups = [
Group(
name: name,
photo: _groupPhotoPath,
subtitle: desc,
members: _members,
),
];
debugPrint(_groups[0].isGroup.toString());
_groups = _localList + _groups;
final String _newGroups = Group.encode(_groups);
await prefs.setString('groups', _newGroups);
var snackBar = SnackBar(
backgroundColor: Colors.green,
content: Text('$name added'),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
}
}
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplex_chat/providers/drawer_providers.dart';
class MyDrawer extends StatelessWidget {
const MyDrawer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final _drawerProviders = Provider.of<DrawerProvider>(context);
return Padding(
padding: const EdgeInsets.all(10.0),
child: Builder(builder: (context) {
return Column(
children: [
const SizedBox(height: 30.0),
SvgPicture.asset(
'assets/logo.svg',
height: 55.0,
),
const Divider(height: 30.0),
ListTile(
leading: const Icon(Icons.insert_invitation),
title: const Text('Invitations'),
onTap: () {
_drawerProviders.currentIndex = 2;
Navigator.pop(context);
},
),
const Spacer(),
ListTile(
leading: const Icon(Icons.refresh),
title: const Text('Switch Profile'),
subtitle: const Text(
'Not supported yet!',
style: TextStyle(fontStyle: FontStyle.italic),
),
onTap: () => _switchProfile(context),
),
],
);
}),
);
}
// remove the locally stored user data
void _switchProfile(BuildContext context) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int _count = 0;
Navigator.of(context).popUntil((route) => _count++ >= 2);
String? _name = prefs.getString('displayName');
await prefs.remove('displayName');
await prefs.remove('fullName');
await prefs.remove('photo$_name');
}
}
@@ -0,0 +1,189 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/providers/drawer_providers.dart';
import 'package:simplex_chat/views/contacts/conversations.dart';
import 'package:simplex_chat/views/home/drawer.dart';
import 'package:simplex_chat/views/invitations/invitation_view.dart';
import 'package:simplex_chat/views/profile/profile_view.dart';
/// Generate [Fake Contacts] by tapping the [Bug Icon]
class HomeView extends StatefulWidget {
const HomeView({Key? key}) : super(key: key);
@override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
String? _photo = '';
String? _displayName = '';
// views
final List<Widget> _views = [
const ProfileView(),
const Conversations(),
const Invitations(),
];
void _getUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_displayName = prefs.getString('displayName');
_photo = prefs.getString('photo$_displayName');
});
}
@override
void initState() {
_getUserData();
super.initState();
}
@override
Widget build(BuildContext context) {
final _drawerProviders = Provider.of<DrawerProvider>(context);
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
drawer: const Drawer(child: MyDrawer()),
body: SafeArea(
child: Builder(builder: (context) {
return Stack(
children: [
_views[_drawerProviders.currentIndex],
_drawerProviders.currentIndex == 0
? Positioned(
top: MediaQuery.of(context).size.height * 0.017,
left: MediaQuery.of(context).size.width * 0.02,
child: InkWell(
onTap: () {
_drawerProviders.currentIndex = 1;
_getUserData();
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_back, color: kPrimaryColor),
),
),
)
: _drawerProviders.currentIndex == 2
? Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () {
_drawerProviders.currentIndex = 1;
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_back,
color: kPrimaryColor)),
),
const Spacer(),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('Hi! $_displayName',
style: kSmallHeadingStyle),
const Text('Good day!'),
],
),
const SizedBox(width: 10.0),
GestureDetector(
onTap: () {
_drawerProviders.currentIndex = 0;
},
child: CircleAvatar(
backgroundColor: Colors.white,
backgroundImage: _photo!.isEmpty
? const AssetImage('assets/dp.png')
as ImageProvider
: FileImage(File(_photo!)),
),
)
],
),
)
: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () {
Scaffold.of(context).openDrawer();
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SvgPicture.asset('assets/menu.svg'),
),
),
const Spacer(),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('Hi! $_displayName',
style: kSmallHeadingStyle),
const Text('Good day!'),
],
),
const SizedBox(width: 10.0),
GestureDetector(
onTap: () {
_drawerProviders.currentIndex = 0;
},
child: CircleAvatar(
backgroundImage: _photo!.isEmpty
? const AssetImage('assets/dp.png')
as ImageProvider
: FileImage(File(_photo!)),
),
)
],
),
),
],
);
}),
),
),
);
}
Future<bool> _onWillPop() async {
return (await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Exit Application', style: kMediumHeadingStyle),
content: const Text('Are You Sure?'),
actions: <Widget>[
TextButton(
child: const Text(
'Yes',
style: TextStyle(
color: Colors.red,
),
),
onPressed: () => SystemNavigator.pop(),
),
TextButton(
child: const Text('No'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
)) ??
false;
}
}
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:simplex_chat/constants.dart';
class Invitations extends StatefulWidget {
const Invitations({Key? key}) : super(key: key);
@override
State<Invitations> createState() => _InvitationsState();
}
class _InvitationsState extends State<Invitations> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
child: Column(
children: [
const SizedBox(height: 40.0),
Row(
children: const [
Icon(Icons.inventory_outlined, color: kPrimaryColor),
SizedBox(width: 8.0),
Text(
'Invitations',
style: kHeadingStyle,
)
],
),
const SizedBox(height: 5.0),
SizedBox(
height: MediaQuery.of(context).size.height * 0.7,
child: const Center(
child: Text(
"You don't have any invitations yet!",
style: kSmallHeadingStyle,
textAlign: TextAlign.center,
),
),
)
],
),
),
);
}
}
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:simplex_chat/app_routes.dart';
import 'package:simplex_chat/constants.dart';
class IntroView extends StatelessWidget {
const IntroView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
'assets/logo.svg',
height: 80.0,
),
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
const Text(
'Complete your profile to begin using SimpleX. Your profile is local to your device and will help identify you to your connections.',
style: kMediumHeadingStyle,
)
],
),
),
),
floatingActionButton: FloatingActionButton(
heroTag: 'welcome',
onPressed: () => Navigator.pushNamed(context, AppRoutes.setupProfile),
child: const Icon(
Icons.arrow_forward,
),
),
);
}
}
@@ -0,0 +1,320 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/widgets/custom_text_field.dart';
class ProfileView extends StatefulWidget {
const ProfileView({Key? key}) : super(key: key);
@override
_ProfileViewState createState() => _ProfileViewState();
}
class _ProfileViewState extends State<ProfileView> {
final _formKey = GlobalKey<FormState>();
// controllers
final TextEditingController _displayNameController = TextEditingController();
final TextEditingController _fullNameController = TextEditingController();
// Image Picker --> DP properties
final imgPicker = ImagePicker();
File? image;
String? _photo = '';
bool _uploading = false;
// image buttons options
final _dpBtnText = ['Remove', 'Gallery', 'Camera'];
final _dpBtnColors = [Colors.red, Colors.purple, Colors.green];
final _dpBtnIcons = [
Icons.delete,
Icons.photo_rounded,
Icons.camera_alt_rounded
];
String? _displayName = '';
String? _fullName = '';
void _getUserData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_fullName = prefs.getString('fullName');
_displayName = prefs.getString('displayName');
_photo = prefs.getString('photo$_displayName');
});
_displayNameController.text = _displayName!;
if (_fullName != null) _fullNameController.text = _fullName!;
}
@override
void initState() {
_getUserData();
super.initState();
}
@override
void dispose() {
_displayNameController.dispose();
_fullNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Size _size = MediaQuery.of(context).size;
return Scaffold(
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: _size.height * 0.05),
Center(
child: SizedBox(
height: 180.0,
width: 180.0,
child: Stack(
children: [
_photo != ''
? CircleAvatar(
backgroundColor: Colors.white,
radius: 100.0,
backgroundImage: FileImage(File(_photo!)),
)
: const CircleAvatar(
backgroundColor: Colors.white,
radius: 100.0,
backgroundImage: AssetImage('assets/dp.png'),
),
Positioned(
right: 0,
bottom: 0,
child: FloatingActionButton(
backgroundColor: kSecondaryColor,
elevation: 2.0,
mini: true,
onPressed: _updateProfilePic,
child: _uploading
? const SizedBox(
height: 18.0,
width: 18.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: const Icon(
Icons.add_a_photo,
size: 20,
),
),
)
],
),
)),
SizedBox(height: _size.height * 0.035),
const Text('Display Name', style: kSmallHeadingStyle),
const SizedBox(height: 10.0),
CustomTextField(
textEditingController: _displayNameController,
textInputType: TextInputType.name,
hintText: 'e.g John',
validatorFtn: (value) {
if (value!.isEmpty) {
return 'Display name cannot be empty';
}
return null;
},
),
SizedBox(height: _size.height * 0.035),
const Text('Full Name', style: kSmallHeadingStyle),
SizedBox(height: _size.height * 0.012),
CustomTextField(
textEditingController: _fullNameController,
textInputType: TextInputType.name,
hintText: 'e.g John Doe',
),
SizedBox(height: _size.height * 0.035),
const Text(
'Your display name is what your contact will know you :)',
style: TextStyle(letterSpacing: 1.2),
)
],
),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
heroTag: 'save',
onPressed: () async {
if (_formKey.currentState!.validate()) {
FocusManager.instance.primaryFocus!.unfocus();
await _createProfile();
const snackBar = SnackBar(
backgroundColor: Colors.green,
content: Text('Profile updated!'),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(snackBar);
}
},
child: const Icon(Icons.check),
),
);
}
// create profile and store in local
Future<void> _createProfile() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('displayName', _displayNameController.text.trim());
await prefs.setString('fullName', _fullNameController.text.trim());
await prefs.setString(
'photo${_displayNameController.text.trim()}', _photo!);
debugPrint(prefs.getString('photo'));
}
void _updateProfilePic() {
Size _size = MediaQuery.of(context).size;
showModalBottomSheet(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0),
topRight: Radius.circular(10.0),
),
),
context: context,
builder: (context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(360.0)),
height: 7.0,
width: 50.0,
),
SizedBox(height: _size.height * 0.025),
const Align(
alignment: Alignment.centerLeft,
child: Text(
' Profile photo',
style: kHeadingStyle,
),
),
SizedBox(height: _size.height * 0.015),
Row(
children: List.generate(
3,
(index) => Column(
mainAxisSize: MainAxisSize.min,
children: [
MaterialButton(
color: _dpBtnColors.map((e) => e).elementAt(index),
shape: const CircleBorder(),
onPressed: index == 0
? () => _removePic()
: index == 1
? () => _galleryPic()
: () => _cameraPic(),
child: Icon(
_dpBtnIcons.map((e) => e).elementAt(index),
color: Colors.white,
),
),
Text(
_dpBtnText.map((e) => e).elementAt(index),
textAlign: TextAlign.center,
)
],
),
))
],
),
),
);
}
void _removePic() {
setState(() {
image = null;
_photo = '';
});
Navigator.pop(context);
}
void _cameraPic() async {
try {
setState(() {
_uploading = true;
});
// picking Image from Camera
final file = await imgPicker.getImage(
source: ImageSource.camera,
);
if (file != null) {
image = File(file.path);
setState(() {
_uploading = false;
_photo = file.path;
});
} else {
setState(() {
_uploading = false;
});
}
Navigator.pop(context);
} catch (e) {
rethrow;
}
}
void _galleryPic() async {
try {
debugPrint('gallery pic');
setState(() {
_uploading = true;
});
// picking Image from local storage
final file = await imgPicker.getImage(
source: ImageSource.gallery,
);
if (file != null) {
image = File(file.path);
setState(() {
_uploading = false;
_photo = file.path;
});
} else {
setState(() {
_uploading = false;
});
}
Navigator.pop(context);
} catch (e) {
rethrow;
}
}
}
@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:share/share.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/widgets/custom_btn.dart';
class ScanInvitationView extends StatelessWidget {
const ScanInvitationView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Size _size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('Share QR Code'),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Display this QR Code for your contact to scan.',
style: kMediumHeadingStyle,
textAlign: TextAlign.center,
),
SizedBox(height: _size.height * 0.04),
GestureDetector(
onTap: () => _showConnection(context),
child:
Image.asset('assets/code.png', height: _size.height * 0.3),
),
SizedBox(height: _size.height * 0.04),
const Text(
'If you cannot share your QR Code, send the invitation via a trusted method.',
style: kMediumHeadingStyle,
textAlign: TextAlign.center,
),
SizedBox(height: _size.height * 0.04),
CustomButton(
width: _size.width * 0.5,
height: _size.height * 0.055,
onPressed: _shareLink,
color: kPrimaryColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.share, color: Colors.white),
SizedBox(width: 8.0),
Text(
'Share',
style: TextStyle(color: Colors.white),
),
],
),
),
],
),
),
),
);
}
void _showConnection(BuildContext context) {
Size _size = MediaQuery.of(context).size;
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
backgroundImage: const AssetImage('assets/dp.png'),
radius: _size.height * 0.085,
),
SizedBox(height: _size.height * 0.045),
const Text(
'Bob wants to connect with you!',
style: kMediumHeadingStyle,
textAlign: TextAlign.center,
),
SizedBox(height: _size.height * 0.045),
CustomButton(
width: _size.width * 0.5,
height: _size.height * 0.055,
onPressed: () {
// work around for now
Navigator.pop(context);
Navigator.of(context).pop(true);
},
color: kPrimaryColor,
child: const Text(
'Confirm',
style: TextStyle(color: Colors.white),
),
),
SizedBox(height: _size.height * 0.013),
CustomButton(
width: _size.width * 0.5,
height: _size.height * 0.055,
onPressed: () {
int _count = 0;
Navigator.popUntil(context, (route) => _count++ >= 2);
},
color: kSecondaryColor,
child: const Text(
'Ignore',
style: TextStyle(color: Colors.white),
),
),
],
),
),
);
}
void _shareLink() {
Share.share('Add me to SimpleX Chat via link https://someLinkHere.io');
}
}
@@ -0,0 +1,290 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplex_chat/app_routes.dart';
import 'package:simplex_chat/constants.dart';
import 'package:simplex_chat/views/home/home_view.dart';
import 'package:simplex_chat/widgets/custom_text_field.dart';
class SetupProfileView extends StatefulWidget {
const SetupProfileView({Key? key}) : super(key: key);
@override
_SetupProfileViewState createState() => _SetupProfileViewState();
}
class _SetupProfileViewState extends State<SetupProfileView> {
final _formKey = GlobalKey<FormState>();
// controllers
final TextEditingController _displayNameController = TextEditingController();
final TextEditingController _fullNameController = TextEditingController();
// Image Picker --> DP properties
final imgPicker = ImagePicker();
File? image;
String photoUrl = '';
bool _uploading = false;
bool _imageUploaded = false;
// image buttons options
final _dpBtnText = ['Gallery', 'Camera'];
final _dpBtnColors = [Colors.purple, Colors.green];
final _dpBtnIcons = [Icons.photo_rounded, Icons.camera_alt_rounded];
@override
void dispose() {
_displayNameController.dispose();
_fullNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 30),
Center(
child: SizedBox(
height: 180.0,
width: 180.0,
child: Stack(
children: [
_imageUploaded
? CircleAvatar(
backgroundColor: Colors.white,
radius: 100.0,
backgroundImage: FileImage(image!),
)
: const CircleAvatar(
backgroundColor: Colors.white,
radius: 100.0,
backgroundImage: AssetImage('assets/dp.png'),
),
Positioned(
right: 0,
bottom: 0,
child: FloatingActionButton(
backgroundColor: kSecondaryColor,
elevation: 2.0,
mini: true,
onPressed: _updateProfilePic,
child: _uploading
? const SizedBox(
height: 18.0,
width: 18.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: const Icon(
Icons.add_a_photo,
size: 20,
),
),
)
],
),
)),
const SizedBox(height: 25.0),
const Text('Display Name', style: kSmallHeadingStyle),
const SizedBox(height: 10.0),
CustomTextField(
textEditingController: _displayNameController,
textInputType: TextInputType.name,
hintText: 'e.g John',
validatorFtn: (value) {
if (value!.isEmpty) {
return 'Display name cannot be empty';
}
return null;
},
),
const SizedBox(height: 25.0),
const Text('Full Name', style: kSmallHeadingStyle),
const SizedBox(height: 10.0),
CustomTextField(
textEditingController: _fullNameController,
textInputType: TextInputType.name,
hintText: 'e.g John Doe',
),
const SizedBox(height: 25.0),
const Text(
'Your display name is what your contact will know you :)',
style: TextStyle(letterSpacing: 1.2),
)
],
),
),
),
),
),
),
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton(
heroTag: 'setup',
onPressed: () async {
if (_formKey.currentState!.validate()) {
FocusScope.of(context).unfocus();
await _createProfile();
await Navigator.pushNamed(context, AppRoutes.home);
_displayNameController.clear();
_fullNameController.clear();
image = null;
}
},
child: const Icon(Icons.check),
),
),
);
}
// create profile and save the data locally to access across the app
Future<void> _createProfile() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('displayName', _displayNameController.text.trim());
await prefs.setString('fullName', _fullNameController.text.trim());
await prefs.setString(
'photo${_displayNameController.text.trim()}', photoUrl);
debugPrint(prefs.getString('photo'));
}
void _updateProfilePic() {
showModalBottomSheet(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0),
topRight: Radius.circular(10.0),
),
),
context: context,
builder: (context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(360.0)),
height: 7.0,
width: 50.0,
),
const SizedBox(height: 20.0),
const Align(
alignment: Alignment.centerLeft,
child: Text(
' Profile photo',
style: kHeadingStyle,
),
),
const SizedBox(height: 15.0),
Row(
children: List.generate(
2,
(index) => Column(
mainAxisSize: MainAxisSize.min,
children: [
MaterialButton(
color: _dpBtnColors.map((e) => e).elementAt(index),
shape: const CircleBorder(),
onPressed:
index == 0 ? () => _galleryPic() : () => _cameraPic(),
child: Icon(
_dpBtnIcons.map((e) => e).elementAt(index),
color: Colors.white,
),
),
Text(
_dpBtnText.map((e) => e).elementAt(index),
textAlign: TextAlign.center,
)
],
),
))
],
),
),
);
}
void _cameraPic() async {
try {
setState(() {
_uploading = true;
});
// picking Image from Camera
final file = await imgPicker.getImage(
source: ImageSource.camera,
);
if (file != null) {
image = File(file.path);
setState(() {
_uploading = false;
_imageUploaded = true;
photoUrl = file.path;
});
} else {
setState(() {
_uploading = false;
});
}
Navigator.pop(context);
} catch (e) {
rethrow;
}
}
void _galleryPic() async {
try {
debugPrint('gallery pic');
setState(() {
_uploading = true;
});
// picking Image from local storage
final file = await imgPicker.getImage(
source: ImageSource.gallery,
);
if (file != null) {
image = File(file.path);
setState(() {
_uploading = false;
_imageUploaded = true;
photoUrl = file.path;
});
} else {
setState(() {
_uploading = false;
});
}
Navigator.pop(context);
} catch (e) {
rethrow;
}
}
}
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:progress_indicators/progress_indicators.dart';
import 'package:simplex_chat/animations/entrance_fader.dart';
import 'package:simplex_chat/app_routes.dart';
import 'package:simplex_chat/constants.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
@override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
// delay on splash screen
void _splashDelay() {
Future.delayed(const Duration(seconds: 4), () {
Navigator.pushNamed(
context,
AppRoutes.intro,
);
});
}
@override
void initState() {
_splashDelay();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
EntranceFader(
duration: const Duration(seconds: 1),
offset: const Offset(0, 15),
child: SvgPicture.asset('assets/logo.svg', height: 85),
),
EntranceFader(
offset: const Offset(0, 0),
duration: const Duration(seconds: 1),
delay: const Duration(seconds: 1),
child: JumpingDotsProgressIndicator(
fontSize: 40,
color: kPrimaryColor,
),
),
],
),
),
);
}
}
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
class CustomButton extends StatelessWidget {
const CustomButton(
{required this.width,
required this.height,
required this.onPressed,
required this.color,
required this.child,
Key? key})
: super(key: key);
final double width;
final double height;
final void Function() onPressed;
final Widget child;
final Color color;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: MaterialButton(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)),
color: color,
onPressed: onPressed,
child: child,
),
);
}
}
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
class CustomTextField extends StatefulWidget {
final TextEditingController textEditingController;
final TextInputType textInputType;
final String hintText;
final String? Function(String?)? validatorFtn;
const CustomTextField({
Key? key,
required this.textEditingController,
required this.textInputType,
required this.hintText,
this.validatorFtn,
}) : super(key: key);
@override
_CustomTextFieldState createState() => _CustomTextFieldState();
}
class _CustomTextFieldState extends State<CustomTextField> {
final FocusNode _node = FocusNode();
@override
void dispose() {
_node.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return Container(
width: width * 0.89,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8.0)),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
controller: widget.textEditingController,
textInputAction: TextInputAction.done,
keyboardType: widget.textInputType,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 15.0),
hintText: widget.hintText,
hintStyle: Theme.of(context).textTheme.caption,
fillColor: Colors.grey[200],
filled: true,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent)),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
),
errorBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
focusedErrorBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
),
validator: widget.validatorFtn,
),
);
}
}
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:simplex_chat/constants.dart';
class MessageBubble extends StatefulWidget {
const MessageBubble({
Key? key,
required this.sender,
required this.text,
required this.isUser,
}) : super(key: key);
final String sender;
final String text;
final bool isUser;
@override
_MessageBubbleState createState() => _MessageBubbleState();
}
class _MessageBubbleState extends State<MessageBubble> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment:
widget.isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.sender,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 2.0),
Material(
borderRadius: widget.isUser
? const BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10))
: const BorderRadius.only(
topRight: Radius.circular(10),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10)),
elevation: 1.0,
color: widget.isUser ? Colors.teal : kPrimaryColor,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
child: Text(
widget.text,
style: const TextStyle(fontSize: 15, color: Colors.white),
),
),
),
],
),
);
}
}
+314
View File
@@ -1,6 +1,20 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.6"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0"
async:
dependency: transitive
description:
@@ -43,6 +57,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1+5"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
cupertino_icons:
dependency: "direct main"
description:
@@ -57,11 +85,39 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
file_picker:
dependency: "direct main"
description:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.4"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.2"
flutter_lints:
dependency: "direct dev"
description:
@@ -69,11 +125,79 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "0.22.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.3"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.8"
image_picker:
dependency: "direct main"
description:
name: image_picker
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.5+4"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
lints:
dependency: transitive
description:
@@ -95,6 +219,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
@@ -102,6 +240,146 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
path_drawing:
dependency: transitive
description:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.3"
progress_indicators:
dependency: "direct main"
description:
name: progress_indicators
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.1"
qr_code_scanner:
dependency: "direct main"
description:
name: qr_code_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.2"
share:
dependency: "direct main"
description:
name: share
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
sky_engine:
dependency: transitive
description: flutter
@@ -156,6 +434,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
url_strategy:
dependency: "direct dev"
description:
name: url_strategy
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
vector_math:
dependency: transitive
description:
@@ -163,5 +448,34 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.9"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.2"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
sdks:
dart: ">=2.14.0 <3.0.0"
flutter: ">=2.5.0"
+29 -4
View File
@@ -1,4 +1,4 @@
name: simplex_app
name: simplex_chat
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.14.0 <3.0.0"
sdk: ">=2.12.0 <3.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -34,6 +34,24 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
progress_indicators: ^1.0.0
# QR Code
qr_code_scanner: ^0.5.2
# for placing svg
flutter_svg: ^0.22.0
# attachments
image_picker: ^0.7.5+3
file_picker: ^3.0.2+2
# state management
provider: ^6.0.1
shared_preferences: ^2.0.8
# share
share: ^2.0.4
dev_dependencies:
flutter_test:
@@ -45,6 +63,13 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_launcher_icons: ^0.9.2
url_strategy: ^0.2.0
flutter_icons:
image_path: "assets/x.png"
android: true
ios: true
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -58,8 +83,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
assets:
- assets/
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
+9 -9
View File
@@ -5,26 +5,26 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:simplex_app/main.dart";
import 'package:simplex_chat/main.dart';
void main() {
testWidgets("Counter increments smoke test", (WidgetTester tester) async {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text("0"), findsOneWidget);
expect(find.text("1"), findsNothing);
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text("0"), findsNothing);
expect(find.text("1"), findsOneWidget);
});
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
}, skip: true);
}
+1
View File
@@ -97,5 +97,6 @@
loadMainDartJs();
}
</script>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.3.1/dist/jsQR.min.js"></script>
</body>
</html>
@@ -1 +1,6 @@
void main() {}
// import 'package:simplexmq/simplexmq.dart';
// void main() {
// var awesome = Awesome();
// print('awesome: ${awesome.isAwesome}');
// }
+1 -1
View File
@@ -4,4 +4,4 @@
library simplexmq;
export 'src/protocol.dart';
export 'src/transport.dart' show Transport, SMPTransportClient;
export 'src/transport.dart' show Transport;
-22
View File
@@ -130,25 +130,3 @@ Uint8List? decode64(Uint8List b64) {
return bytes;
}
Uint8List encodeInt32(int n) {
final data = Uint8List(4);
ByteData.sublistView(data).setInt32(0, n);
return data;
}
Uint8List encodeInt16(int n) {
final data = Uint8List(2);
ByteData.sublistView(data).setInt16(0, n);
return data;
}
extension EqualUint8List on Uint8List {
bool equal(Uint8List b) {
if (length != b.length) return false;
for (int i = 0; i < length; i++) {
if (this[i] != b[i]) return false;
}
return true;
}
}
-101
View File
@@ -1,101 +0,0 @@
import 'dart:math' show Random;
import 'dart:typed_data';
import 'package:pointycastle/api.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:pointycastle/asymmetric/oaep.dart';
import 'package:pointycastle/asymmetric/rsa.dart';
import 'package:pointycastle/block/aes_fast.dart';
import 'package:pointycastle/block/modes/gcm.dart';
import 'package:pointycastle/digests/sha256.dart';
import 'package:pointycastle/key_generators/api.dart';
import 'package:pointycastle/key_generators/rsa_key_generator.dart';
import 'package:pointycastle/random/fortuna_random.dart';
import 'package:pointycastle/signers/pss_signer.dart';
import 'buffer.dart';
class AESKey {
final Uint8List _key;
AESKey._make(this._key);
static AESKey random([bool secure = false]) =>
AESKey._make((secure ? secureRandomBytes : pseudoRandomBytes)(32));
static AESKey decode(Uint8List rawKey) => AESKey._make(rawKey);
Uint8List get bytes => _key;
}
Uint8List randomIV() => pseudoRandomBytes(16);
Uint8List secureRandomBytes(int len) => _randomBytes(len, Random.secure());
final sessionSeed = Random.secure();
Uint8List pseudoRandomBytes(int len) => _randomBytes(len, sessionSeed);
Uint8List _randomBytes(int len, Random seedSource) {
final bytes = Uint8List(len);
for (int i = 0; i < len; i++) {
bytes[i] = seedSource.nextInt(256);
}
return bytes;
}
final paddingByte = '#'.codeUnitAt(0);
Uint8List encryptAES(AESKey key, Uint8List iv, int padTo, Uint8List data) {
if (data.length >= padTo) throw ArgumentError('large message');
final padded = Uint8List(padTo);
padded.setAll(0, data);
padded.fillRange(data.length, padTo, paddingByte);
return _makeGCMCipher(key, iv, true).process(padded);
}
Uint8List decryptAES(AESKey key, Uint8List iv, Uint8List encryptedAndTag) =>
_makeGCMCipher(key, iv, false).process(encryptedAndTag);
GCMBlockCipher _makeGCMCipher(AESKey key, Uint8List iv, bool encrypt) =>
GCMBlockCipher(AESFastEngine())
..init(encrypt, AEADParameters(KeyParameter(key._key), 128, iv, empty));
FortunaRandom _secureFortunaRandom() =>
FortunaRandom()..seed(KeyParameter(secureRandomBytes(32)));
AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> generateRSAkeyPair(
[int bitLength = 2048]) {
final keyGen = RSAKeyGenerator()
..init(ParametersWithRandom(
RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64),
_secureFortunaRandom()));
final pair = keyGen.generateKeyPair();
return AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(
pair.publicKey as RSAPublicKey, pair.privateKey as RSAPrivateKey);
}
Uint8List encryptOAEP(RSAPublicKey key, Uint8List data) =>
_oaep(true, PublicKeyParameter<RSAPublicKey>(key)).process(data);
Uint8List decryptOAEP(RSAPrivateKey key, Uint8List data) =>
_oaep(false, PrivateKeyParameter<RSAPrivateKey>(key)).process(data);
OAEPEncoding _oaep(bool encrypt, AsymmetricKeyParameter keyParam) =>
OAEPEncoding.withSHA256(RSAEngine())..init(encrypt, keyParam);
Uint8List signPSS(RSAPrivateKey privateKey, Uint8List data) =>
_pss(true, PrivateKeyParameter<RSAPrivateKey>(privateKey))
.generateSignature(data)
.bytes;
bool verifyPSS(RSAPublicKey publicKey, Uint8List data, Uint8List sig) {
try {
return _pss(false, PublicKeyParameter<RSAPublicKey>(publicKey))
.verifySignature(data, PSSSignature(sig));
} on ArgumentError {
return false;
}
}
PSSSigner _pss(bool sign, AsymmetricKeyParameter keyParam) => PSSSigner(
RSAEngine(), SHA256Digest(), SHA256Digest())
..init(sign,
ParametersWithSaltConfiguration(keyParam, _secureFortunaRandom(), 32));
-24
View File
@@ -13,11 +13,9 @@ final charUpperA = cc('A');
final charUpperZ = cc('Z');
final charPlus = cc('+');
final charSlash = cc('/');
final charDot = cc('.');
class Parser {
final Uint8List _s;
final List<int> _positions = [];
int _pos = 0;
bool _fail = false;
Parser(this._s);
@@ -36,20 +34,6 @@ class Parser {
return res;
}
T? tryParse<T>(T? Function(Parser p) parse) {
if (_fail || _pos >= _s.length) {
_fail = true;
return null;
}
_positions.add(_pos);
final res = parse(this);
final prevPos = _positions.removeLast();
if (res == null) {
_pos = prevPos;
_fail = false;
}
}
// takes a required number of bytes
Uint8List? take(int len) => _run(() {
final end = _pos + len;
@@ -86,14 +70,6 @@ class Parser {
return true;
});
// takes the passed char
bool? char(int c) => _run(() {
if (_s[_pos] == c) {
_pos++;
return true;
}
});
// takes space
bool? space() => _run(() {
if (_s[_pos] == charSpace) {
+1 -1
View File
@@ -174,7 +174,7 @@ class ERR extends BrokerCommand {
: cmdErr = err == ErrorType.CMD
? throw ArgumentError('CMD error should be created with ERR.CMD')
: null;
ERR.cmd(CmdErrorType this.cmdErr) : err = ErrorType.CMD;
ERR.cmd(this.cmdErr) : err = ErrorType.CMD;
@override
Uint8List serialize() {
final _err = errorTags[err]!;
-109
View File
@@ -1,109 +0,0 @@
import 'dart:typed_data';
import 'package:asn1lib/asn1lib.dart';
import 'package:pointycastle/asymmetric/api.dart'
show RSAPublicKey, RSAPrivateKey;
const _rsaOid = '1.2.840.113549.1.1.1';
final _asnRsaOid = ASN1ObjectIdentifier.fromComponentString(_rsaOid);
final _asn1Null = ASN1Null();
ASN1Sequence _asn1Sequence(List<ASN1Object> elements) {
final seq = ASN1Sequence()..elements = elements;
return seq;
}
void _assertRsaAlgorithm(ASN1Sequence seq) {
if (seq.elements.isEmpty ||
seq.elements[0] is! ASN1ObjectIdentifier ||
(seq.elements[0] as ASN1ObjectIdentifier).identifier != _rsaOid) {
throw Exception('Invalid key algorithm identifier');
}
}
/// Decodes binary PKCS1 to [RSAPublicKey]
RSAPublicKey decodeRsaPubKeyPKCS1(Uint8List bytes) {
final els = ASN1Sequence.fromBytes(bytes).elements;
if (els.length != 2 || els[0] is! ASN1Integer || els[1] is! ASN1Integer) {
throw Exception('Invalid PKCS1 encoding');
}
return RSAPublicKey(
(els[0] as ASN1Integer).valueAsBigInteger!,
(els[1] as ASN1Integer).valueAsBigInteger!,
);
}
/// Decodes binary SPKI to [RSAPublicKey]
RSAPublicKey decodeRsaPubKey(Uint8List bytes) {
final els = ASN1Sequence.fromBytes(bytes).elements;
if (els.length != 2 || els[1] is! ASN1BitString || els[0] is! ASN1Sequence) {
throw Exception('Invalid SPKI structure');
}
_assertRsaAlgorithm(els[0] as ASN1Sequence);
return decodeRsaPubKeyPKCS1(els[1].valueBytes().sublist(1));
}
/// Encodes [key] as binary PKCS1
Uint8List encodeRsaPubKeyPKCS1(RSAPublicKey key) =>
_asn1Sequence([ASN1Integer(key.modulus!), ASN1Integer(key.publicExponent!)])
.encodedBytes;
/// Encodes [key] as binary SPKI
Uint8List encodeRsaPubKey(RSAPublicKey key) => _asn1Sequence([
_asn1Sequence([_asnRsaOid, _asn1Null]),
ASN1BitString(encodeRsaPubKeyPKCS1(key))
]).encodedBytes;
/// Decodes binary PKCS1 to [RSAPrivateKey]
RSAPrivateKey decodeRsaPrivKeyPKCS1(Uint8List bytes) {
final els = ASN1Sequence.fromBytes(bytes).elements;
if (els.length != 9 || els.any((el) => el is! ASN1Integer)) {
throw Exception('Invalid PKCS1 encoding');
}
return RSAPrivateKey(
(els[1] as ASN1Integer).valueAsBigInteger!,
(els[3] as ASN1Integer).valueAsBigInteger!,
(els[4] as ASN1Integer).valueAsBigInteger!,
(els[5] as ASN1Integer).valueAsBigInteger!);
}
/// Decodes binary PKCS8 to [RSAPrivateKey]
RSAPrivateKey decodeRsaPrivKey(Uint8List bytes) {
final els = ASN1Sequence.fromBytes(bytes).elements;
if (els.length != 3 ||
els[1] is! ASN1Sequence ||
els[2] is! ASN1OctetString) {
throw Exception('Invalid PKCS8 structure');
}
_assertRsaAlgorithm(els[1] as ASN1Sequence);
return decodeRsaPrivKeyPKCS1(els[2].valueBytes());
}
final _asnZero = ASN1Integer(BigInt.from(0));
/// Encodes [key] as PKCS1 binary
Uint8List encodeRsaPrivKeyPKCS1(RSAPrivateKey key) {
final d = key.privateExponent!;
final p = key.p!;
final q = key.q!;
final dModP = d % (p - BigInt.from(1));
final dModQ = d % (q - BigInt.from(1));
final coefficient = q.modInverse(p);
return _asn1Sequence([
_asnZero,
ASN1Integer(key.modulus!),
ASN1Integer(key.publicExponent!),
ASN1Integer(d),
ASN1Integer(p),
ASN1Integer(q),
ASN1Integer(dModP),
ASN1Integer(dModQ),
ASN1Integer(coefficient),
]).encodedBytes;
}
/// Encodes [key] as PKCS8 binary
Uint8List encodeRsaPrivKey(RSAPrivateKey key) => _asn1Sequence([
_asnZero,
_asn1Sequence([_asnRsaOid, _asn1Null]),
ASN1OctetString(encodeRsaPrivKeyPKCS1(key))
]).encodedBytes;
+6 -299
View File
@@ -1,310 +1,17 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:pointycastle/digests/sha256.dart';
import 'buffer.dart';
import 'crypto.dart';
import 'parser.dart';
import 'protocol.dart';
import 'rsa_keys.dart';
abstract class Transport {
Future<Uint8List> read(int n);
Future<void> write(Uint8List data);
Future<void> close();
}
class SMPServer {
final String host;
final int? port;
final Uint8List? keyHash;
SMPServer(this.host, [this.port, this.keyHash]);
}
class SessionKey {
final AESKey aesKey;
final Uint8List baseIV;
int _counter = 0;
SessionKey._(this.aesKey, this.baseIV);
static SessionKey create() => SessionKey._(AESKey.random(), randomIV());
Uint8List serialize() => concat(aesKey.bytes, baseIV);
}
class ServerHeader {
final int blockSize;
final int keySize;
ServerHeader(this.blockSize, this.keySize);
}
class ServerHandshake {
final RSAPublicKey publicKey;
final int blockSize;
ServerHandshake(this.publicKey, this.blockSize);
}
typedef SMPVersion = List<int>;
SMPVersion _currentSMPVersion = const [0, 4, 1, 0];
const int _serverHeaderSize = 8;
const int _binaryRsaTransport = 0;
const int _transportBlockSize = 4096;
const int _maxTransportBlockSize = 65536;
class _Request {
final Uint8List queueId;
final Completer<BrokerResponse> completer = Completer();
_Request(this.queueId);
}
class Either<L, R> {
final L? left;
final R? right;
Either.left(L this.left) : right = null;
Either.right(R this.right) : left = null;
}
enum ClientErrorType { SMPServerError, SMPResponseError, SMPUnexpectedResponse }
class BrokerResponse {
final BrokerCommand? command;
final ClientErrorType? errorType;
final ERR? error;
BrokerResponse(BrokerCommand this.command)
: errorType = null,
error = null;
BrokerResponse.error(ClientErrorType this.errorType, this.error)
: command = null;
}
class ClientTransmission {
final String corrId;
final Uint8List queueId;
final ClientCommand command;
ClientTransmission(this.corrId, this.queueId, this.command);
Uint8List serialize() =>
unwordsN([encodeAscii(corrId), encode64(queueId), command.serialize()]);
}
class BrokerTransmission {
final String corrId;
final Uint8List queueId;
final BrokerCommand? command;
final ERR? error;
BrokerTransmission(this.corrId, this.queueId, BrokerCommand this.command)
: error = null;
BrokerTransmission.error(this.corrId, this.queueId, ERR this.error)
: command = null;
}
final badBlock = BrokerTransmission.error('', empty, ERR(ErrorType.BLOCK));
class SMPTransportClient {
final Transport _conn;
final _sndKey = SessionKey.create();
final _rcvKey = SessionKey.create();
final int blockSize;
int _corrId = 0;
bool _messageStreamCreated = false;
final Map<String, _Request> _sentCommands = {};
SMPTransportClient._(this._conn, this.blockSize);
static Future<SMPTransportClient> connect(Transport conn,
{Uint8List? keyHash, int? blockSize}) {
return _clientHandshake(conn, keyHash, blockSize);
}
Future<void> close() {
return _conn.close();
}
Future<BrokerResponse> sendSMPCommand(
RSAPrivateKey? key, Uint8List queueId, ClientCommand cmd) async {
final corrId = (++_corrId).toString();
final t = ClientTransmission(corrId, queueId, cmd).serialize();
final sig = key == null ? empty : encode64(signPSS(key, t));
final data = unwordsN([sig, t, empty]);
final r = _sentCommands[corrId] = _Request(queueId);
await _writeEncrypted(data);
return r.completer.future;
}
Stream<BrokerTransmission> messageStream() {
if (_messageStreamCreated) {
throw Exception('message stream already created');
Stream<Uint8List> blockStream(Transport t, int blockSize) async* {
try {
while (true) {
yield await t.read(blockSize);
}
_messageStreamCreated = true;
return _messageStream();
}
Stream<BrokerTransmission> _messageStream() async* {
try {
while (true) {
final block = await _readEncrypted();
final t = _parseBrokerTransmission(block);
if (t.corrId == '') {
yield t;
} else {
final r = _sentCommands.remove(t.corrId);
if (r == null) {
yield t;
} else {
final cmd = t.command;
r.completer.complete(r.queueId.equal(t.queueId)
? cmd == null
? BrokerResponse.error(
ClientErrorType.SMPResponseError, t.error)
: cmd is ERR
? BrokerResponse.error(
ClientErrorType.SMPServerError, cmd)
: BrokerResponse(cmd)
: BrokerResponse.error(
ClientErrorType.SMPUnexpectedResponse, null));
}
}
}
} catch (e) {
return;
}
}
static BrokerTransmission _parseBrokerTransmission(Uint8List s) {
final p = Parser(s);
p.space();
final cId = p.word();
p.space();
final queueId = p.tryParse((p) => p.base64()) ?? empty;
p.space();
if (p.fail || cId == null) return badBlock;
final corrId = decodeAscii(cId);
final command = smpCommandP(p);
if (command == null) {
return BrokerTransmission.error(
corrId, queueId, ERR.cmd(CmdErrorType.SYNTAX));
}
if (command is! BrokerCommand) {
return BrokerTransmission.error(
corrId, queueId, ERR.cmd(CmdErrorType.PROHIBITED));
}
final qErr = _tQueueError(queueId, command);
if (qErr != null) {
return BrokerTransmission.error(corrId, queueId, ERR.cmd(qErr));
}
return BrokerTransmission(corrId, queueId, command);
}
static CmdErrorType? _tQueueError(Uint8List queueId, BrokerCommand cmd) {
if (cmd is IDS || cmd is PONG) {
if (queueId.isNotEmpty) return CmdErrorType.HAS_AUTH;
} else if (cmd is! ERR && queueId.isEmpty) {
return CmdErrorType.NO_QUEUE;
}
}
static Future<SMPTransportClient> _clientHandshake(
Transport conn, Uint8List? keyHash, int? blkSize) async {
final srv = await _getHeaderAndPublicKey_1_2(conn, keyHash);
final t = SMPTransportClient._(conn, blkSize ?? srv.blockSize);
await t._sendEncryptedKeys_4(srv.publicKey);
_checkVersion(await t._getWelcome_6());
return t;
}
static Future<ServerHandshake> _getHeaderAndPublicKey_1_2(
Transport conn, Uint8List? keyHash) async {
final srvHeader = parseServerHeader(await conn.read(_serverHeaderSize));
final blkSize = srvHeader.blockSize;
if (blkSize < _transportBlockSize || blkSize > _maxTransportBlockSize) {
throw Exception('smp handshake header error: bad block size $blkSize');
}
final rawKey = await conn.read(srvHeader.keySize);
if (keyHash != null) validateKeyHash_2(rawKey, keyHash);
final serverKey = decodeRsaPubKey(rawKey);
return ServerHandshake(serverKey, blkSize);
}
static void validateKeyHash_2(Uint8List rawKey, Uint8List keyHash) {
if (keyHash.equal(SHA256Digest().process(rawKey))) return;
throw Exception('smp handshake error: bad key hash');
}
static ServerHeader parseServerHeader(Uint8List a) {
if (a.length != 8) {
throw Exception('smp handshake error: bad header size ${a.length}');
}
final v = ByteData.sublistView(a);
final blockSize = v.getUint32(0);
final transportMode = v.getUint16(4);
if (transportMode != _binaryRsaTransport) {
throw Exception('smp handshake error: bad transport mode $transportMode');
}
final keySize = v.getUint16(6);
return ServerHeader(blockSize, keySize);
}
Future<void> _sendEncryptedKeys_4(RSAPublicKey serverKey) async {
final header = encryptOAEP(serverKey, _clientHeader());
await _conn.write(header);
}
Uint8List _clientHeader() => concatN([
encodeInt32(blockSize),
encodeInt16(_binaryRsaTransport),
_sndKey.serialize(),
_rcvKey.serialize()
]);
Future<SMPVersion> _getWelcome_6() async =>
_parseSMPVersion(await _readEncrypted());
static SMPVersion _parseSMPVersion(Uint8List block) {
final p = Parser(block);
final SMPVersion version = [0, 0, 0, 0];
void setVer(int i, int? v) {
if (v == null || p.fail) {
throw Exception('smp handshake error: bad version format');
}
version[i] = v;
}
for (var i = 0; i < 3; i++) {
final v = p.decimal();
p.char(charDot);
setVer(i, v);
}
final v = p.decimal();
p.space();
setVer(3, v);
return version;
}
static void _checkVersion(SMPVersion srvVersion) {
final s0 = srvVersion[0];
final c0 = _currentSMPVersion[0];
if (s0 > c0 || (s0 == c0 && srvVersion[1] > _currentSMPVersion[1])) {
throw Exception('smp handshake error: incompatible server version');
}
}
Future<Uint8List> _readEncrypted() async {
final block = await _conn.read(blockSize);
final iv = _nextIV(_rcvKey);
return decryptAES(_rcvKey.aesKey, iv, block);
}
Future<void> _writeEncrypted(Uint8List data) {
final iv = _nextIV(_sndKey);
final block = encryptAES(_sndKey.aesKey, iv, blockSize - 16, data);
return _conn.write(block);
}
static Uint8List _nextIV(SessionKey sk) {
final c = encodeInt32(sk._counter++);
final start = sk.baseIV.sublist(0, 4);
final rest = sk.baseIV.sublist(4);
for (int i = 0; i < 4; i++) {
start[i] ^= c[i];
}
return concat(start, rest);
} catch (e) {
return;
}
}
+2 -7
View File
@@ -6,14 +6,9 @@ publish_to: none
environment:
sdk: '>=2.14.0 <3.0.0'
dependencies:
asn1lib: ^1.0.2
pointycastle:
git:
url: https://github.com/simplex-chat/pc-dart.git
ref: nullsafety
# pointycastle: ^3.3.4
# dependencies:
# path: ^1.8.0
dev_dependencies:
lints: ^1.0.0
-29
View File
@@ -1,29 +0,0 @@
import 'package:simplexmq/src/buffer.dart';
import 'package:simplexmq/src/crypto.dart';
import 'package:test/test.dart';
void main() {
group('AES-GCM encryption with padding', () {
test('encrypt and decrypt', () {
final key = AESKey.random();
final iv = pseudoRandomBytes(16);
final data = encodeAscii('hello');
final cipherText = encryptAES(key, iv, 32, data);
expect(cipherText.length, 32 + 16);
final decrypted = decryptAES(key, iv, cipherText);
expect(decodeAscii(decrypted),
'hello' + List.filled(32 - 'hello'.length, '#').join());
});
});
group('RSA-OAEP encryption', () {
test('encrypt and decrypt', () {
final keyPair = generateRSAkeyPair();
final data = encodeAscii('hello there');
final cipherText = encryptOAEP(keyPair.publicKey, data);
expect(cipherText.length, 2048 ~/ 8);
final decrypted = decryptOAEP(keyPair.privateKey, cipherText);
expect(decodeAscii(decrypted), 'hello there');
});
});
}
@@ -1,23 +0,0 @@
import 'package:simplexmq/src/buffer.dart';
import 'package:simplexmq/src/rsa_keys.dart';
import 'package:test/test.dart';
void main() {
group('RSA keys encoding', () {
test('SPKI encode/decode RSA public key', () {
final keyStr =
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fdE2lndTnPi7QHS2OqP1YE6ZH6sGdf7Boji6jPgQVJB289aQAPZRSJlg6s+xHC52sa2isFiuZN2uFENNWznuZsOWXkHMthbo9Qkp7ZjOhomZURtsIsaRny9GTcaFOrd19rqbsrCRLyb3xtwbQjv/2HEGNZyP9YsGsZijTJaV0yQNEp/5Gt3jHebJ8mqLdBr/aDQBf3oSsmUDDvocGU4kL14GOuVYCKNlEUrFe1X1poSXLH0uu485GVfHB72XjKP/flS2rL91fguqMil1nkelL1K4WOyx1Z87LyyXT2Vh4GRLVHG/a9LyPpw7ovQlO5RIr6suODkXwbAUHq/8j5IDwIDAQAB';
final key = decodeRsaPubKey(decode64(encodeAscii(keyStr))!);
final encKey = decodeAscii(encode64(encodeRsaPubKey(key)));
expect(encKey, keyStr);
});
test('PKCS8 encode/decode RSA private key', () {
final keyStr =
'MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZ90TaWd1Oc+LtAdLY6o/VgTpkfqwZ1/sGiOLqM+BBUkHbz1pAA9lFImWDqz7EcLnaxraKwWK5k3a4UQ01bOe5mw5ZeQcy2Fuj1CSntmM6GiZlRG2wixpGfL0ZNxoU6t3X2upuysJEvJvfG3BtCO//YcQY1nI/1iwaxmKNMlpXTJA0Sn/ka3eMd5snyaot0Gv9oNAF/ehKyZQMO+hwZTiQvXgY65VgIo2URSsV7VfWmhJcsfS67jzkZV8cHvZeMo/9+VLasv3V+C6oyKXWeR6UvUrhY7LHVnzsvLJdPZWHgZEtUcb9r0vI+nDui9CU7lEivqy44ORfBsBQer/yPkgPAgMBAAECggEAezFPgB4EgB/tpUk/k4xXiTPF/iC+QskYvyPFJNv3JtRIFuWGO+Iw/esn9xhlnH+d+/IOIDSXCQ44ropY7dZEzlm97YIDOJCikuEHaqciRCedheT8Hikwy6Aa/NJw8lug0SyRDdeZn2H+s0X98BJ6Gxx1yhgCcOQq/2MbNnS8LNQ0yNNHu9Ds2K5Weiwhb9nrLLuMvrF/k1z0QNi5mCzDZK8iDMr87UZycmKKue13/xppI1pddJm4Ta13/OmZtYe2d5UgK9FrLStFkl7yqWnIcDueCOZvqo4nIfxPlPVolQ9B9RXL2tctkYRVy6FBkZIJkSk4O1Vz5BuPgBy9McoRMQKBgQD5xmVKAnvBQ08BUGXw5HVy4qR1Oj/EmxXKNypZsTdaKpiMfjNB8GbegO0na9Ry7sRo5g15vXpjCS6LDwGx4fZ/5u4N5CP3845DscrZjebs0tS7u5USPMoMzZ/KYfddRsGdm7y9HMp3Z6O3ZGGqZ15VFHGYjRjzK8BYE91rvv1WlwKBgQDfZe0c8/aa25KHW2zLRtSOR8ze+pz79hxNmpCeOoyzfaVRzwDh3KUl78PEWqMbJ7EdEdokCisFU0yEpuuc29JD6l9YuQmYH2VGdyg52iPPCXJOP1PBO0VQ/D/cAmZd/75cmoiyC7kELHfiAWBqO/7xpWkNiEpZZcI33DbzReYBSQKBgBKGuaqUppM+J9UEHpuQhnmf/+zGBkbR7frSvqxqbZ2dfTUmgyzH5Qlp7K043UgtF5pkPemiuToxSyd7VHfaN8ti2JNlMZnJkerJfC9IzDESrj7CehshMSdj9Q8w1wUvI1tKWuR4Bzh2Enme03OtORz8aDSVep1GyHx/9LNyNh4/AoGAcpdk/nIB8ENrMTVrZAYsJ+OaqlIhTnla4U/EmPVtkPCFaaZmTHUS3ZfUcpcPjXFZv5CVteDlWnD1EiJRP3/epmnFiMw5qKeKGpAquSo1LhEpagu/2aGel8EcvK0ad2Mk8XlvXuz2dbads/eCzluCFdAESCW+BYdWDbNPGJClP8kCgYBoT+0res0efi1cn6H0fPx/q33Wmgf47txVrzQN0ZEWDFOOhnErvGpRan9AG+LGvp7TvWWHnW13qjFCXGocWcbaoqsLabkov961R8ij2MTeToz6V+7YfK0KBt/h2HHJ5t/CybNxE5iYFyUMI7GTlC2GzFrnvxH/UYwUma1AplkpEw==';
final key = decodeRsaPrivKey(decode64(encodeAscii(keyStr))!);
final encKey = decodeAscii(encode64(encodeRsaPrivKey(key)));
expect(encKey, keyStr);
});
});
}
@@ -1,12 +1,6 @@
import 'dart:io';
// import 'package:simplexmq_io/simplexmq_io.dart';
void main() {
ServerSocket.bind('localhost', 8080)
.then((server) => server.listen((Socket socket) {
print('New client connection');
socket.listen((List<int> data) {
String result = String.fromCharCodes(data);
print(result);
});
}));
}
// void main() {
// var awesome = Awesome();
// print('awesome: ${awesome.isAwesome}');
// }
+12 -9
View File
@@ -20,22 +20,22 @@ class SocketTransport implements Transport {
final int _bufferSize;
Uint8List _buffer = Uint8List(0);
final ListQueue<_STReaders> _readers = ListQueue(16);
SocketTransport._(this._socket, this._timeout, this._bufferSize);
SocketTransport._new(this._socket, this._timeout, this._bufferSize);
static Future<SocketTransport> connect(String host, int port,
{Duration timeout = const Duration(seconds: 4),
{Duration timeout = const Duration(seconds: 1),
int bufferSize = 16384}) async {
final socket = await Socket.connect(host, port, timeout: timeout);
final t = SocketTransport._(socket, timeout, bufferSize);
final t = SocketTransport._new(socket, timeout, bufferSize);
// ignore: cancel_subscriptions
final subscription = socket.listen(t._onData,
onError: (Object e) => t.close, onDone: t.close);
onError: (Object e) => t._finalize, onDone: t._finalize);
t._subscription = subscription;
return t;
}
@override
Future<Uint8List> read(int n) {
Future<Uint8List> read(int n) async {
if (_readers.isEmpty && _buffer.length >= n) {
final data = _buffer.sublist(0, n);
_buffer = _buffer.sublist(n);
@@ -71,14 +71,17 @@ class SocketTransport implements Transport {
}
}
/// Close the client transport
@override
Future<void> close() async {
await _subscription.cancel();
void _finalize() {
_subscription.cancel();
_socket.destroy();
while (_readers.isNotEmpty) {
final r = _readers.removeFirst();
r.completer.completeError(Exception('socket closed'));
}
}
/// Allow closing the client transport.
void close() {
_finalize();
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies:
git:
url: git://github.com/simplex-chat/simplex-chat
path: packages/simplexmq
ref: ep/smp-client
ref: ep/socket-transport
dependency_overrides:
simplexmq:
@@ -12,7 +12,7 @@ void main() {
group('transport', () {
Future<ServerSocket> startServer(
void Function(Socket client) handleConnection) async {
var server = await ServerSocket.bind(localhost, 8080);
var server = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
server.listen(handleConnection);
return server;
}
@@ -27,11 +27,11 @@ void main() {
},
);
});
var transport = await SocketTransport.connect(localhost, 8080);
var transport = await SocketTransport.connect(localhost, server.port);
await transport.write(Uint8List.fromList([1, 2, 3]));
expect(await completer.future, [1, 2, 3]);
await transport.close();
transport.close();
await server.close();
});
@@ -41,7 +41,7 @@ void main() {
});
var transport = await SocketTransport.connect(localhost, server.port);
expect(await transport.read(3), [1, 2, 3]);
await transport.close();
transport.close();
await server.close();
});
});
@@ -1,38 +0,0 @@
import 'package:simplexmq/simplexmq.dart';
import 'package:simplexmq/src/buffer.dart';
import 'package:simplexmq/src/crypto.dart';
import 'package:simplexmq/src/rsa_keys.dart';
import 'package:simplexmq_io/simplexmq_io.dart';
import 'package:test/test.dart';
final keyHash =
decode64(encodeAscii('pH7bg7B6vB3uJ1poKmClTAqr7yYWnAtapnIDN7ypKxU='));
void main() {
group('SMP transport (expects SMP server on localhost:5223)', () {
test('establish connection', () async {
final conn = await SocketTransport.connect('localhost', 5223);
final smp = await SMPTransportClient.connect(conn, keyHash: keyHash);
expect(smp is SMPTransportClient, true);
});
test('should create SMP queue and send message', () async {
final conn1 = await SocketTransport.connect('localhost', 5223);
final alice = await SMPTransportClient.connect(conn1, keyHash: keyHash);
final aliceMessages = alice.messageStream();
final aliceKeys = generateRSAkeyPair();
final rcvKeyBytes = encodeRsaPubKey(aliceKeys.publicKey);
// input stream is not processed without the call to listen()
aliceMessages.listen((_) {});
// final conn2 = await SocketTransport.connect('localhost', 5223);
// final bob = await SMPTransportClient.connect(conn2, keyHash: keyHash);
// final bobKeys = generateRSAkeyPair();
// final sndKeyStr = encode64(encodeRsaPubKey(bobKeys.publicKey));
final resp = await alice.sendSMPCommand(
aliceKeys.privateKey, empty, NEW(rcvKeyBytes));
expect(resp.command is IDS, true);
});
// });
}, skip: 'requires SMP server on port 5223');
}