Compare commits
497 Commits
Author | SHA1 | Date | |
---|---|---|---|
06ab90de1e | |||
6e4207f517 | |||
bc0dcbdbb1 | |||
a994d9978c | |||
30b3dc6921 | |||
b1b7772532 | |||
f8910c8f8b | |||
e70aaabbc9 | |||
ea45bf828c | |||
2a00530126 | |||
ad2cf6d4f9 | |||
230cb2c018 | |||
819e2a7590 | |||
fb80f3bd52 | |||
f9db9aa632 | |||
a630a5ae79 | |||
ed9f5e396c | |||
4614f3ae2e | |||
d10b1d0d22 | |||
99ae726c0a | |||
642f5e11fc | |||
cbbda7237b | |||
0b2f939376 | |||
fdec22c28a | |||
bd73e265cc | |||
5d0ead5889 | |||
ba60fa9e37 | |||
f54cc22fc6 | |||
d8b2dd2599 | |||
7ccc7a492e | |||
1e0e2fca52 | |||
dbb2a3f1a1 | |||
bd5ed8fb33 | |||
3546bacc83 | |||
201200299c | |||
c1196a6359 | |||
bedc3f5277 | |||
5a25769b71 | |||
05c806b358 | |||
70eb088756 | |||
b0cfeec513 | |||
e35a0d2fd4 | |||
e638398b2e | |||
f3626f233f | |||
ece9164d93 | |||
e7b1beca50 | |||
6fc1a263d2 | |||
b84eba59e3 | |||
8f7ca14586 | |||
19d4e1d31c | |||
701d5d3c27 | |||
ec4ca238de | |||
f70717a987 | |||
e02ab259b6 | |||
a4181e3d42 | |||
858f81d05e | |||
46affd4e68 | |||
3518594eea | |||
8f2574a555 | |||
3257fd865f | |||
f9502d1700 | |||
b9babd43a8 | |||
c8ca80f6e7 | |||
217111e3fd | |||
8705aa1b0d | |||
0458d5431c | |||
75a80b1018 | |||
52d217a89c | |||
1f1ed0cda4 | |||
6c00e0bcab | |||
2989e98c50 | |||
08c77340a0 | |||
a23b76b552 | |||
dacccf57b5 | |||
b094361f5a | |||
1ee9a2c5cc | |||
25a25e4c70 | |||
d4a0748249 | |||
1ea286f3ef | |||
a32e968992 | |||
0cd9371460 | |||
1b0a3fd24b | |||
54e37b3e69 | |||
2519adeef4 | |||
4f9001cb2b | |||
613ceadfaa | |||
459757b292 | |||
e399f71a78 | |||
c5d1512375 | |||
16ec9a8e00 | |||
c19cbaac88 | |||
277c08048d | |||
482e938744 | |||
1d0bd45632 | |||
3a39387365 | |||
4d885affb9 | |||
0ad8d5c393 | |||
e5ed4fadda | |||
581059cb1d | |||
0c526abfe8 | |||
f3ccb4e89a | |||
24765789df | |||
52cc8407d5 | |||
523f9fd0e3 | |||
9ce1a401d9 | |||
a07c6cd415 | |||
a3e5b1a70b | |||
5e51fbe617 | |||
7c82aa5edf | |||
0db7de01ee | |||
ee6e28426c | |||
df915e1ca3 | |||
e33cdf56a9 | |||
bbcc3c9d02 | |||
6db9eade90 | |||
c759b54024 | |||
138d70a6e1 | |||
4e2974a2e2 | |||
e7073292b5 | |||
06aa84e3a1 | |||
ac2736dd23 | |||
9c53572f94 | |||
b337e632bd | |||
e0d69ab504 | |||
5086f45cc1 | |||
acc81acdea | |||
b4465cc70c | |||
ef0ae2a586 | |||
9697ba8149 | |||
d70c2751bb | |||
cac23dd620 | |||
3794eca68b | |||
e3c11e48b2 | |||
cfc3552011 | |||
f8ba06d0ae | |||
6528ef5bc2 | |||
bf2ba03912 | |||
e7181c1f59 | |||
9b0c1323b6 | |||
61098ffc43 | |||
b364c7aece | |||
288cd492a2 | |||
736accaca4 | |||
922fce541f | |||
bc3972082c | |||
fbe22c5031 | |||
7f1130aca8 | |||
313e7a4b3c | |||
e231f26231 | |||
9a2efaae1c | |||
67e881af0d | |||
a6ce969e89 | |||
d04b40f662 | |||
21d844b2ba | |||
4c11aa9753 | |||
dfcc49eab4 | |||
395b9aca6b | |||
70e3efc93d | |||
fe3f0c0e85 | |||
6b8cc2569d | |||
17f271dfb9 | |||
c747d3c1ba | |||
57e504540a | |||
9e5e93da39 | |||
f7bf69fbfd | |||
78ffc38534 | |||
466803a1ac | |||
39f4e8c78c | |||
4c749ec6ac | |||
620ad0d5cf | |||
f3eda1c89d | |||
a01ebe7d8a | |||
f4ced0324c | |||
5c26626f92 | |||
e7f360c6fc | |||
773cf17da0 | |||
1bd2bdd1aa | |||
c65558ef6c | |||
1309e7ad3a | |||
096fd42f22 | |||
27a56ae533 | |||
e3130c9e4b | |||
260002270f | |||
a119f60fdb | |||
a53ae381dc | |||
6bdaf83563 | |||
476f08681b | |||
cf9f93dcb3 | |||
1d7e846973 | |||
ef186f79d2 | |||
b40308c9e4 | |||
63a8e61e80 | |||
25222e9156 | |||
642820127c | |||
44f417a0f2 | |||
ce908d1b51 | |||
4db9191fd8 | |||
c262e40e81 | |||
27176531cf | |||
b23aa782b8 | |||
4367dcc728 | |||
6d95d5f7a2 | |||
db52c495ec | |||
32aa73a951 | |||
67c217715a | |||
f45bcd691e | |||
099f282cd6 | |||
032815b29f | |||
123eaad296 | |||
b020895a8c | |||
2cea14a012 | |||
38f0257fa3 | |||
150529a740 | |||
6cbe34379e | |||
782e6bc978 | |||
e7e81da05d | |||
af8e558d9f | |||
109ba3f04b | |||
68b4c79960 | |||
5ccd3d4884 | |||
6cb9fd97e2 | |||
fe17f81a40 | |||
b0fd0d7d51 | |||
13cd3186f5 | |||
3835ec4fe1 | |||
33eb0c5aed | |||
af079cc4b0 | |||
2311af8219 | |||
a64a192d27 | |||
5fb845732f | |||
935d7dbb63 | |||
ccb1547523 | |||
f41983304a | |||
edff7868f2 | |||
c7f970ae70 | |||
97db56ae78 | |||
89d3b79617 | |||
8943ae8144 | |||
de063bd797 | |||
24e08d63f8 | |||
1750d6079e | |||
0b1dfa460a | |||
cd82fac803 | |||
d8fa90fc6a | |||
fe163b3c69 | |||
86575a1e86 | |||
87e670a520 | |||
91be430dbb | |||
f31323fe23 | |||
6961694fd2 | |||
0103779608 | |||
5c4020d511 | |||
968321b12c | |||
eabb27495c | |||
263849266f | |||
c5c544fb34 | |||
286639889b | |||
3d0bfe6c3f | |||
d3132942bc | |||
28451eddbd | |||
86c89e782c | |||
5c76f6e0a6 | |||
e3c9105b1c | |||
0c2f3a28d0 | |||
47dbb90640 | |||
e00f452b98 | |||
11c25ea271 | |||
44ea647624 | |||
3ffb24f7c5 | |||
e78d526bbb | |||
91e7dd4019 | |||
6e274cae11 | |||
e1795bac03 | |||
d4a39a3527 | |||
a48e7f57a6 | |||
07adf8c2ca | |||
685565e031 | |||
eb66ea407b | |||
2edaedc5f3 | |||
3c7795837c | |||
2ec8693e85 | |||
804457c761 | |||
1227ef283c | |||
f179e7e1d5 | |||
e2bf5e73dd | |||
bff73dbb21 | |||
df111e393a | |||
9646cb7a70 | |||
4cb672cb16 | |||
1a53a26f39 | |||
f450a46e99 | |||
d6f2df7002 | |||
dbf2ed868a | |||
2d86780f0b | |||
cc08ed0232 | |||
971210e7e8 | |||
8f0f50d0e4 | |||
519c68b092 | |||
d889321b38 | |||
257523b526 | |||
0bb9be9a72 | |||
4c1e33a264 | |||
e204c62ba9 | |||
9b427b9683 | |||
02e60ad87c | |||
20ea964337 | |||
289bf30a40 | |||
29cc8558c3 | |||
d64d2ece05 | |||
ae9491e198 | |||
3cf9ee39d0 | |||
5d432a5f87 | |||
c0e2516f39 | |||
25f72bd11c | |||
225df61aa0 | |||
e6df696077 | |||
032b247080 | |||
c42fe7f806 | |||
75e15f9f83 | |||
88ea7e2431 | |||
f32ee89924 | |||
cd355f84cc | |||
38c1474d2e | |||
9e5decae61 | |||
cf0708cd3b | |||
991d7ec1ab | |||
6ba89b6bd1 | |||
2109c71dd6 | |||
da38ce426f | |||
8f927e9f72 | |||
ca1f94531f | |||
24f5a3e482 | |||
c09670ad64 | |||
ace7b44595 | |||
9a4b61aff3 | |||
1b5169eb56 | |||
8bd937420e | |||
e1f8ad1466 | |||
cbc0de944e | |||
da48328c92 | |||
bb62a3a159 | |||
4b0b41a902 | |||
c65d87dacc | |||
6b8e1f205b | |||
b2c4f33665 | |||
622258802b | |||
d79e132420 | |||
4e951c6a78 | |||
8af4a61072 | |||
ee5f791638 | |||
9a991b221c | |||
ba07247ec4 | |||
60c135bf01 | |||
3a9bb3d13e | |||
2cf017ad2d | |||
ae391ab691 | |||
de84fb6113 | |||
675d1b8588 | |||
7768e36c62 | |||
0ebccf1075 | |||
d94b535001 | |||
707577f9ac | |||
a74600ce4b | |||
d083728251 | |||
565f351d1e | |||
b9a329c8f0 | |||
a16618bb51 | |||
8609e4e169 | |||
e5ccedd180 | |||
6a9e0e36c1 | |||
de1dceae9b | |||
2927f72674 | |||
2858c50449 | |||
4988c8fea8 | |||
167217a5a0 | |||
7364fc49a8 | |||
ee6509bb9a | |||
e05323c3bb | |||
096251eaad | |||
45d903bcf7 | |||
473402149a | |||
80f05cb008 | |||
73c7fa8807 | |||
c0856c5126 | |||
99ecc399ee | |||
0f99e807f8 | |||
456df166b1 | |||
08d357fc72 | |||
9154fe47e1 | |||
08c2ac32aa | |||
04693cc163 | |||
391d3150dd | |||
da641515fa | |||
0a03f581d1 | |||
f227209e9b | |||
5eaf8d6b72 | |||
159e7228bf | |||
40cf1b1ddc | |||
cb47f0351e | |||
909e68e7bb | |||
88ba2d303e | |||
2746623b8d | |||
b0be889833 | |||
dab4e7bde1 | |||
6b08b62832 | |||
a60c1ed68c | |||
02034acbbe | |||
469e1e1f92 | |||
526f698bf4 | |||
a8c358fd58 | |||
0700014b3a | |||
1b13a90615 | |||
36f89a9a53 | |||
de3cd9c7b7 | |||
9021ca7168 | |||
7549a9ff22 | |||
6239c10579 | |||
78e75cffdb | |||
2c1ae783e3 | |||
8b6e464644 | |||
003eb1efc2 | |||
90d58bbee3 | |||
025e5bdf1c | |||
31dfec89ee | |||
9b53a3b0c9 | |||
03c4a4eae0 | |||
d822e0edd8 | |||
b326507417 | |||
414f81b32f | |||
bffdab1423 | |||
0494a9058d | |||
ef9a2c7190 | |||
7e01947da9 | |||
7bb805bffd | |||
ef9510e731 | |||
6e0f6d1d79 | |||
8feea380a4 | |||
7071600c3f | |||
806c6fbc2b | |||
90cbd7ab23 | |||
0ec0f216e2 | |||
32c491ae84 | |||
3fa45f9744 | |||
69f8710f31 | |||
824be11013 | |||
6e96a554ff | |||
3bbb09e813 | |||
cecf18f5a0 | |||
7014ded7f0 | |||
45d3f93192 | |||
2bb75da017 | |||
7de882338d | |||
3389ca18f7 | |||
b7be59bc6e | |||
ec1732088a | |||
302e5f22ce | |||
c7502e6a04 | |||
8881ef3af4 | |||
f5914a8d34 | |||
d6f0147339 | |||
cf5b1180a9 | |||
f0a23bcb47 | |||
e777c4c991 | |||
f8a70faf28 | |||
dde909457d | |||
2d26395d73 | |||
5f70669a84 | |||
add1712b7d | |||
566f205dc9 | |||
bd33e1f9c2 | |||
b10163575f | |||
c7d8843f06 | |||
b52748a93b | |||
8d49a80e79 | |||
cd677deec0 | |||
2c9dc5199c | |||
eaeb07db69 | |||
d0bafd5bd9 | |||
4bedbc4b25 | |||
8300fc8ca9 | |||
f16480092b | |||
c0b64580f9 | |||
b397fe33b7 | |||
8655d20234 | |||
96a3c05497 | |||
bed6539d5d | |||
6e867041e8 | |||
13394811bb | |||
a19579d4fd | |||
7e817106b1 | |||
1be499f242 | |||
527927da82 | |||
bcf9a1586a | |||
c714d24dfa | |||
a603d5bd3a | |||
32a32224ca | |||
0162224b7f |
3
.gitignore
vendored
@ -68,3 +68,6 @@
|
|||||||
!**/ios/**/default.pbxuser
|
!**/ios/**/default.pbxuser
|
||||||
!**/ios/**/default.perspectivev3
|
!**/ios/**/default.perspectivev3
|
||||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||||
|
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
|
||||||
|
19
Makefile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
beta_online_release:
|
||||||
|
flutter build apk --flavor beta -t lib/main_online.dart
|
||||||
|
|
||||||
|
beta_dev_release:
|
||||||
|
flutter build apk --flavor beta -t lib/main_dev.dart
|
||||||
|
|
||||||
|
|
||||||
|
beta_online_release_split_per_abi:
|
||||||
|
flutter build apk --flavor beta -t lib/main_online.dart --target-platform android-arm,android-arm64,android-x64 --split-per-abi
|
||||||
|
|
||||||
|
|
||||||
|
stable_release_split_per_abi:
|
||||||
|
flutter build apk --flavor stable -t lib/main_online.dart --target-platform android-arm,android-arm64,android-x64 --split-per-abi
|
||||||
|
|
||||||
|
stable_release:
|
||||||
|
flutter build apk --flavor stable -t lib/main_online.dart
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: beta_offline_release beta_online_release_split_per_abi stable_release_split_per_abi
|
@ -33,7 +33,15 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 30
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
|
||||||
|
// Required to use WebRTC
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
@ -42,11 +50,11 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "org.communiquons.comunic"
|
applicationId "org.communiquons.comunic"
|
||||||
minSdkVersion 16
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 30
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -63,6 +71,10 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
|
|
||||||
|
useProguard true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +89,10 @@ android {
|
|||||||
applicationId "org.communiquons.beta"
|
applicationId "org.communiquons.beta"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
@ -85,6 +101,6 @@ flutter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
}
|
}
|
||||||
|
3
android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## Flutter WebRTC
|
||||||
|
-keep class com.cloudwebrtc.webrtc.** { *; }
|
||||||
|
-keep class org.webrtc.** { *; }
|
@ -3,7 +3,9 @@
|
|||||||
package="org.communiquons.comunic">
|
package="org.communiquons.comunic">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
tools:replace="android:label"
|
android:label="Comunic Beta"
|
||||||
android:label="Comunic Beta" />
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:replace="android:label" />
|
||||||
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
BIN
android/app/src/beta/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 10 KiB |
17
android/app/src/beta/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="0.33269298"
|
||||||
|
android:scaleY="0.33269298"
|
||||||
|
android:translateX="35.369194"
|
||||||
|
android:translateY="20.2176">
|
||||||
|
<group android:translateY="158.50157">
|
||||||
|
<path android:pathData="M79,-42L107,-33.578125Q104.1875,-21.8125,98.15625,-13.921875Q92.140625,-6.03125,83.1875,-2.015625Q74.25,2,60.4375,2Q43.671875,2,33.046875,-2.890625Q22.421875,-7.796875,14.703125,-20.140625Q7,-32.484375,7,-51.75Q7,-77.421875,20.5625,-91.203125Q34.140625,-105,58.953125,-105Q78.375,-105,89.484375,-97.140625Q100.59375,-89.296875,106,-73.046875L78,-67Q76.53125,-71.546875,74.921875,-73.65625Q72.265625,-77.1875,68.421875,-79.09375Q64.578125,-81,59.828125,-81Q49.0625,-81,43.328125,-72.265625Q39,-65.796875,39,-51.921875Q39,-34.75,44.140625,-28.375Q49.296875,-22,58.625,-22Q67.65625,-22,72.28125,-27.125Q76.90625,-32.25,79,-42Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
16
android/app/src/beta/res/drawable/launch_background.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#3c3f40" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/splash_icon" />
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
BIN
android/app/src/beta/res/drawable/splash_icon.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
android/app/src/beta/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/beta/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/beta/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/beta/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
android/app/src/beta/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
android/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
android/app/src/beta/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#3C3F41</color>
|
||||||
|
</resources>
|
@ -4,4 +4,7 @@
|
|||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
|
<!-- Use clear connection in dev mode -->
|
||||||
|
<application android:usesCleartextTraffic="true" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -5,17 +5,43 @@
|
|||||||
<!-- Internet connection is required to access to the API -->
|
<!-- Internet connection is required to access to the API -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<!-- These permissions are required to make video calls (WebRTC) -->
|
||||||
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
|
||||||
|
<!-- This is required on Android 11+ for image picker -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.VIDEO_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
In most cases you can leave this as-is, but you if you want to provide
|
In most cases you can leave this as-is, but you if you want to provide
|
||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
|
||||||
android:label="Comunic"
|
android:label="Comunic"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@drawable/ic_app_rounded"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
@ -27,13 +53,28 @@
|
|||||||
until Flutter renders its first frame. It can be removed if
|
until Flutter renders its first frame. It can be removed if
|
||||||
there is no splash screen (such as the default splash screen
|
there is no splash screen (such as the default splash screen
|
||||||
defined in @style/LaunchTheme). -->
|
defined in @style/LaunchTheme). -->
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:value="true" />
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Specify that the launch screen should continue being displayed -->
|
||||||
|
<!-- until Flutter renders its first frame. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||||
|
android:resource="@drawable/launch_background" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- This let the image cropper work -->
|
||||||
|
<activity
|
||||||
|
android:name="com.yalantis.ucrop.UCropActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 12 KiB |
@ -1,13 +1,6 @@
|
|||||||
package org.communiquons.comunic;
|
package org.communiquons.comunic;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
import io.flutter.app.FlutterActivity;
|
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
|
||||||
|
|
||||||
public class MainActivity extends FlutterActivity {
|
public class MainActivity extends FlutterActivity {
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
GeneratedPluginRegistrant.registerWith(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 10 KiB |
17
android/app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="0.33269298"
|
||||||
|
android:scaleY="0.33269298"
|
||||||
|
android:translateX="35.369194"
|
||||||
|
android:translateY="20.2176">
|
||||||
|
<group android:translateY="158.50157">
|
||||||
|
<path android:pathData="M79,-42L107,-33.578125Q104.1875,-21.8125,98.15625,-13.921875Q92.140625,-6.03125,83.1875,-2.015625Q74.25,2,60.4375,2Q43.671875,2,33.046875,-2.890625Q22.421875,-7.796875,14.703125,-20.140625Q7,-32.484375,7,-51.75Q7,-77.421875,20.5625,-91.203125Q34.140625,-105,58.953125,-105Q78.375,-105,89.484375,-97.140625Q100.59375,-89.296875,106,-73.046875L78,-67Q76.53125,-71.546875,74.921875,-73.65625Q72.265625,-77.1875,68.421875,-79.09375Q64.578125,-81,59.828125,-81Q49.0625,-81,43.328125,-72.265625Q39,-65.796875,39,-51.921875Q39,-34.75,44.140625,-28.375Q49.296875,-22,58.625,-22Q67.65625,-22,72.28125,-27.125Q76.90625,-32.25,79,-42Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
@ -1,12 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@android:color/white" />
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#1C227E" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
<!-- You can insert your own image assets here -->
|
||||||
<!-- <item>
|
<item>
|
||||||
<bitmap
|
<bitmap
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:src="@mipmap/launch_image" />
|
android:src="@drawable/splash_icon" />
|
||||||
</item> -->
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
BIN
android/app/src/main/res/drawable/splash_icon.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 2.0 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.3 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#1BA1E2</color>
|
||||||
|
</resources>
|
@ -5,4 +5,9 @@
|
|||||||
Flutter draws its first frame -->
|
Flutter draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">@android:color/white</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -5,7 +5,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
classpath 'com.android.tools.build:gradle:3.5.4'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1,4 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableR8=true
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#Fri Jun 23 08:50:38 CEST 2017
|
#Tue Mar 24 12:27:30 CET 2020
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||||
|
1
android/settings_aar.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
include ':app'
|
3
assets/langs.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"fr_FR": "fr"
|
||||||
|
}
|
85
assets/langs/de.json
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"%days%d": "%days% T",
|
||||||
|
"%hours% h": "%hours% h",
|
||||||
|
"%mins% min": "%mins% Min",
|
||||||
|
"%months% months": "%months% Monate",
|
||||||
|
"%num% likes": "%num% Personen gefällt das",
|
||||||
|
"%num% members": "%num% Mitglieder",
|
||||||
|
"%secs%s": "%sec% s",
|
||||||
|
"%years% years": "%years% Jahre",
|
||||||
|
"1 Like": "1 Person gefällt das",
|
||||||
|
"1 member": "1 Mitglied",
|
||||||
|
"1 month": "1 Monat",
|
||||||
|
"1 year": "1 Jahr",
|
||||||
|
"A network error occured!": "Ein Netzfelher ist augetreten!",
|
||||||
|
"Accept": "Annehmen",
|
||||||
|
"Account created": "Account geschafft",
|
||||||
|
"An account is already associated to this email address!": "Diese E-Mail-Adresse wird schon mit einem Konto verbindet!",
|
||||||
|
"An error occured while creating your account. Please try again.": "Während der Schaffung des Kontos ist ein Felher aufgetreten. Versuchen Sie bitte noch einmal.",
|
||||||
|
"App settings": "Anwendungseinstellungen",
|
||||||
|
"Cancel": "Abbrechen",
|
||||||
|
"Confirm": "Bestätigen",
|
||||||
|
"Confirm operation": "Den Einstatz bestätigen",
|
||||||
|
"Confirm your password": "Das Passwort bestätigen",
|
||||||
|
"Could not get conversation information!": "Die Nachrichten des Gesprächs haben nicht wiedererlangt werden gekannt!",
|
||||||
|
"Create a conversation": "Ein Gespräch schaffen",
|
||||||
|
"Create an account": "Ein Konto schaffen",
|
||||||
|
"Delete": "Löschen",
|
||||||
|
"Delete friend": "Einen Freund löschen",
|
||||||
|
"Do you really want to sign out from the application ?": "Wollen Sie tatsächlich ",
|
||||||
|
"Email address": "E-Mail-Adresse",
|
||||||
|
"Enable dark theme": "Das dunkle Thema anwenden",
|
||||||
|
"Error": "Fehler",
|
||||||
|
"Error while creating your account": "Während der Schaffung ihres Kontos ist ein Felher aufgetreten.",
|
||||||
|
"First name": "Vorname",
|
||||||
|
"Follow": "Folgen",
|
||||||
|
"Follow conversation": "Das Gespräch folgen",
|
||||||
|
"Following": "Gefolgt",
|
||||||
|
"Friends": "Freunden",
|
||||||
|
"Friends of %name%": "Freunden von %name%",
|
||||||
|
"Friends only": "Nur für die Freunde",
|
||||||
|
"Group members only": "Nur für die Mitglieder",
|
||||||
|
"I have read and accepted the Terms Of Service.": "Ich habe die Nutzungsbedingungen gelesen und angenommen.",
|
||||||
|
"Invalid credentials!": "Ungültigen Anmeldedaten!",
|
||||||
|
"Invalid email address!": "Ungültiger E-Mail-Adresse!",
|
||||||
|
"Invalid first name!": "Ungültige Vorname!",
|
||||||
|
"Invalid last name!": "Ungültiger Nachname!",
|
||||||
|
"Invalid password!": "Ungültiges Passwort!",
|
||||||
|
"Last name": "Nachname",
|
||||||
|
"Like": "Gefällt mir",
|
||||||
|
"Loading": "Laden ausstehend",
|
||||||
|
"Login": "Einloggen",
|
||||||
|
"Me only": "Nur für mich",
|
||||||
|
"Menu": "Menü",
|
||||||
|
"New message": "Neue Meldung",
|
||||||
|
"Newest": "Neuest",
|
||||||
|
"OK": "OK",
|
||||||
|
"Ok": "Ok",
|
||||||
|
"Online": "Angeschlossen",
|
||||||
|
"PDF": "PDF",
|
||||||
|
"Password": "Passwort",
|
||||||
|
"Please sign into your Comunic account: ": "Einloggen Sie Sie bitte in ihrem Comunic-Konto",
|
||||||
|
"Post": "Beitrag",
|
||||||
|
"Public": "Publik",
|
||||||
|
"Reject": "Absagen",
|
||||||
|
"Remove": "Löschen",
|
||||||
|
"Retry": "Versuchen noch einmal",
|
||||||
|
"Send": "Senden",
|
||||||
|
"Send a message": "Eine Meldung senden",
|
||||||
|
"Sign in": "Sich einloggen",
|
||||||
|
"Sign out": "Log-out",
|
||||||
|
"The password and its confirmation do not match!": "Das Passwort und seine Bestätigung vergleichen nicht!",
|
||||||
|
"This account is private.": "Dieses Konto ist privat.",
|
||||||
|
"Too many accounts have been created from this IP address for now. Please try again later.": "Zu viele Konten sind bisher mit dieser IP-Adresse geshafft worden. Versuchen Sie bitte später noch einmal.",
|
||||||
|
"Too many unsuccessfull login attempts! Please try again later...": "Zu vielen erfolglose Anmeldeversuche! Versuchen Sie bitte später noch einmal...",
|
||||||
|
"Try again": "Noch einmal versuchen",
|
||||||
|
"Update message": "Die Meldung ändern",
|
||||||
|
"You must accept the Terms Of Service to continue.": "Sie müssen die Nutzungsbedingungen lesen, um weiterzugehen.",
|
||||||
|
"You will need to restart the application to apply changes": "Sie müssen die Anwendung neu starten, um die Veränderungen anzusetzen",
|
||||||
|
"Your account has been successfully created. You can now login to start to use it.": "Ihr Konto ist erfolgreich geschafft worden. Sie können Sie jetzt anmelden und es verwalten.",
|
||||||
|
"cancel": "abbrechen",
|
||||||
|
"created a new post": "hat einen Beitrag geschafft",
|
||||||
|
"delete": "bestätigen",
|
||||||
|
"https://www.youtube.com/watch/?v=": "https://www.youtube.com/watch/?v=",
|
||||||
|
"posted a comment": "hat einen Kommentar gepostet"
|
||||||
|
}
|
551
assets/langs/fr.json
Normal file
@ -0,0 +1,551 @@
|
|||||||
|
{
|
||||||
|
"%1% : %2%": "%1% : %2%",
|
||||||
|
"%1% added %2% to the conversation": "%1% a ajouté %2% à la conversation",
|
||||||
|
"%1% and %2% are writing...": "%1% et %2% sont en train d'écrire...",
|
||||||
|
"%1% created the conversation": "%1% a créé la conversation",
|
||||||
|
"%1% is writing...": "%1% est en train d'écrire...",
|
||||||
|
"%1% left the conversation": "%1% a quitté la conversation",
|
||||||
|
"%1% removed %2% from the conversation": "%1% a retiré %2% de la conversation",
|
||||||
|
"%days% Days %hours% Hours %minutes% Minutes %seconds% Seconds": "\"%days% Jours %hours% Heures %minutes% Minutes %seconds% Secondes\"",
|
||||||
|
"%days%d": "%days% j",
|
||||||
|
"%hours% h": "%hours% h",
|
||||||
|
"%mins% min": "%mins% min",
|
||||||
|
"%months% months": "%months% mois",
|
||||||
|
"%num% like": "%num% J'aime",
|
||||||
|
"%num% likes": "%num% personnes aiment",
|
||||||
|
"%num% members": "%num% membres",
|
||||||
|
"%secs%s": "%secs% s",
|
||||||
|
"%years% years": "%years% ans",
|
||||||
|
"'%1%' copied to clipboard!": "'%1%' copié dans le presse papier !",
|
||||||
|
"1 Like": "1 personne aime",
|
||||||
|
"1 member": "1 membre",
|
||||||
|
"1 month": "1 mois",
|
||||||
|
"1 year": "1 an",
|
||||||
|
"10 years": "10 ans",
|
||||||
|
"15 days": "15 jours",
|
||||||
|
"2 years": "2 ans",
|
||||||
|
"3 months": "3 mois",
|
||||||
|
"5 years": "5 ans",
|
||||||
|
"50 years": "50 ans",
|
||||||
|
"6 months": "6 mois",
|
||||||
|
"7 days": "7 jours",
|
||||||
|
":yourShortcut:": ":votreRaccourcis:",
|
||||||
|
"A network error occured!": "Une erreur de réseau s'est produite !",
|
||||||
|
"A registration is required to access this group page.": "Une inscription est nécessaire pour accéder à cette page de groupe !",
|
||||||
|
"ALL": "TOUS",
|
||||||
|
"About this application": "A propos de cette application",
|
||||||
|
"Accept": "Accepter",
|
||||||
|
"Accept request": "Accepter la demande",
|
||||||
|
"Access restrictions": "Restrictions d'accès",
|
||||||
|
"Accessible to everyone, including non-Comunic users": "Accessible par tout le monde, y compris les personnes extérieures à Comunic",
|
||||||
|
"Account created": "Compte créé",
|
||||||
|
"Account image": "Image de compte",
|
||||||
|
"Account image visibility": "Visibilité de l'image de compte",
|
||||||
|
"Account image visiblity": "Visibilité de votre image de compte",
|
||||||
|
"Add": "Ajouter",
|
||||||
|
"Add image": "Ajouter une image",
|
||||||
|
"Add member": "Ajouter un membre",
|
||||||
|
"Add new emoji": "Ajouter un nouvel émoticon",
|
||||||
|
"Admin": "Admin",
|
||||||
|
"Administrator": "Administrateur",
|
||||||
|
"All members": "Tous les membres",
|
||||||
|
"All the members of the group can create posts on the group": "Tous les membres du groupe peuvent créer des posts",
|
||||||
|
"Allow all members of the conversation to add users": "Autoriser tous les membres de la conversation d'ajouter des utilisateurs",
|
||||||
|
"Allow comments on your page": "Autoriser les commentaires sur votre page",
|
||||||
|
"Allow comunic to send emails": "Autoriser Comunic à envoyer des mails",
|
||||||
|
"Allow posts from your friends on your page": "Autoriser les posts de vos amis sur votre page",
|
||||||
|
"Allow users to create new choices": "Autoriser les utilisateurs à créer de nouveaux choix",
|
||||||
|
"An account is already associated to this email address!": "Un compte est déjà associer à cette addresse e-mail !",
|
||||||
|
"An error occured while creating your account. Please try again.": "Une erreur s'est produite lors de la création du compte. Veuillez ré-essayer.",
|
||||||
|
"An error occurred while checking your options !": "Erreur lors de la récupération de vos options de récupération !",
|
||||||
|
"An error occurred while checking your recovery options !": "Erreur lors de la récupération de vos options de récupération !",
|
||||||
|
"An error occurred while creating your account. Please try again.": "Erreur lors de la création de votre compte. Veuillez réessayer",
|
||||||
|
"Answer %num%": "Réponse %num%",
|
||||||
|
"Answer 1": "Réponse 1",
|
||||||
|
"Answer 2": "Réponse 2",
|
||||||
|
"Answer your security questions": "Répondre à vos questions de sécurité",
|
||||||
|
"App settings": "Paramètres de l'application",
|
||||||
|
"Appearance": "Apparence",
|
||||||
|
"Application settings": "Paramètres de l'application",
|
||||||
|
"Are you sure do you want to remove this friend from your list of friends ? A friendship request will have to be sent to get this user back to your list!": "Voulez-vous vraiment supprimer cet ami de votre liste d'amis ? Il faudra une demande d'ami pour réintégrer cet utilisateur à votre liste !",
|
||||||
|
"Audio Player": "Lecteur audio",
|
||||||
|
"Audio record": "Enregistrement audio",
|
||||||
|
"Automatically delete unread notifications after": "Supprimer automatiquement les notifications non lues après",
|
||||||
|
"Automatically delete your account if you have been inactive for": "Supprimer votre compte si vous avez été déconnecté pendant",
|
||||||
|
"Automatically delete your comments after": "Supprimer automatiquement vos commentaires après",
|
||||||
|
"Automatically delete your conversation messages after": "Supprimer automatiqumenet vos messages de conversations après",
|
||||||
|
"Automatically delete your likes after": "Supprimer automatiquement vos \"J'aime\" après",
|
||||||
|
"Automatically delete your posts after": "Supprimer automatiquement vos posts après",
|
||||||
|
"Block the creation of new responses": "Bloquer la création de nouvelles réponses",
|
||||||
|
"Browse files": "Parcourir les fichiers",
|
||||||
|
"Camera": "Caméra",
|
||||||
|
"Can access to all group posts": "Peut accéder à tous les posts du groupe",
|
||||||
|
"Can always create posts, invite users and respond to membership request": "Peut toujours envoyer des posts, inviter des utilisateurs et répondre aux demande d'inscription au groupe",
|
||||||
|
"Can change members privileges and group settings": "Peut changer les privilèges des utilisateurs et les paramètres du groupe",
|
||||||
|
"Cancel": "Annuler",
|
||||||
|
"Cancel request": "Annuler la demande",
|
||||||
|
"Cancel response to survey": "Annuler la réponse au sondage",
|
||||||
|
"Change account image visibility": "Changer la visibilité de l'image de compte",
|
||||||
|
"Change level": "Changer le niveau",
|
||||||
|
"Change logo": "Changer",
|
||||||
|
"Change password": "Changer le mot de passe",
|
||||||
|
"Change your password": "Changer de mot de passe",
|
||||||
|
"Change your security questions": "Changer les questions de sécurité",
|
||||||
|
"Checking availability...": "Vérification de la disponibilité...",
|
||||||
|
"Choose a new password": "Choisir un nouveau mot de passe",
|
||||||
|
"Choose a user": "Choisir un utilisateur",
|
||||||
|
"Choose a video": "Choisir une vidéo",
|
||||||
|
"Choose a virtual directory": "Choisir un répertoire virtuel",
|
||||||
|
"Choose an image": "Choisir une image",
|
||||||
|
"Close": "Fermer",
|
||||||
|
"Closed registration": "Inscription fermée",
|
||||||
|
"Comunic": "Comunic",
|
||||||
|
"Comunic is a free and OpenSource social network that respect your privacy.": "Comunic est un réseau social libre qui respecte votre vie privée.",
|
||||||
|
"Comunic users": "Les utilisateurs de Comunic",
|
||||||
|
"Configure the main settings of your account": "Configurer les paramètres principaux de votre compte",
|
||||||
|
"Confirm": "Confirmer",
|
||||||
|
"Confirm deletion": "Confirmer la suppression",
|
||||||
|
"Confirm operation": "Confirmer l'opération",
|
||||||
|
"Confirm you new password": "Confirmer le mot de passe",
|
||||||
|
"Confirm your password": "Confirmer le mot de passe",
|
||||||
|
"Congratulations! Your password has now been successfully changed!": "Félicitations ! Votre mot de passe a bien été changé !",
|
||||||
|
"Connected users": "Utilisateurs connectés",
|
||||||
|
"Conversation color (optional)": "Couleur de conversation (optionnel)",
|
||||||
|
"Conversation logo": "Logo de la conversation",
|
||||||
|
"Conversation members": "Membres de la conversation",
|
||||||
|
"Conversation name (optional)": "Nom de la conversation (optionnel)",
|
||||||
|
"Conversation name (optionnal)": "Nom de la conversation (optionnel)",
|
||||||
|
"Conversations": "Conversations",
|
||||||
|
"Copy URL": "Copier l'URL",
|
||||||
|
"Copy message": "Copier le message",
|
||||||
|
"Could not block the creation of new choices!": "Erreur lors du bloquage de la création de nouveaux choix !",
|
||||||
|
"Could not cancel invitation!": "Erreur lors de l'annulation de l'invitation !",
|
||||||
|
"Could not cancel your membership request!": "Erreur lors de l'annulation de votre demande à rejoindre ce groupe !",
|
||||||
|
"Could not cancel your response to the survey !": "Impossible d'annuler votre réponse au sondage !",
|
||||||
|
"Could not change group membership level!": "Erreur lors du changement du niveau d'inscription au groupe !",
|
||||||
|
"Could not change your password!": "Erreur lors du changement de votre mot de passe !",
|
||||||
|
"Could not connect to a remote peer!": "Erreur lors de la connexion à un membre de l'appel !",
|
||||||
|
"Could not connect to server!": "Echec de la connexion au serveur",
|
||||||
|
"Could not create a new choice for this survey!": "Erreur lors de la création d'un nouveau choix pour ce sondage !",
|
||||||
|
"Could not create a new group!": "Erreur lors de la création du groupe !",
|
||||||
|
"Could not create comment!": "Impossible de créer le commentaire ! ",
|
||||||
|
"Could not create post !": "Impossible de créer le post !",
|
||||||
|
"Could not create the conversation!": "Impossible de créer la conversation !",
|
||||||
|
"Could not delete all your notifications!": "Erreur lors de la suppression de vos notifications !",
|
||||||
|
"Could not delete conversation message!": "Impossible de supprimer le message de la conversation !",
|
||||||
|
"Could not delete custom emoji!": "Erreur lors de la suppression de l'émoticon personnalisé !",
|
||||||
|
"Could not delete group logo!": "Erreur lors de la suppression du logo du groupe !",
|
||||||
|
"Could not delete the comment!": "Impossible de supprimer le commentaire !",
|
||||||
|
"Could not delete the conversation!": "Impossible de supprimer la conversation !",
|
||||||
|
"Could not delete the group": "Erreur lors de la suppression du groupe !",
|
||||||
|
"Could not delete the post!": "Impossible de supprimer le post !",
|
||||||
|
"Could not delete this person from your friends list!": "Impossible de supprimer cet utilisateur de votre liste d'amis !",
|
||||||
|
"Could not delete user account image!": "Erreur lors de la suppression de votre image de compte !",
|
||||||
|
"Could not delete your account!": "Erreur lors de la suppression de votre compte !",
|
||||||
|
"Could not disconnect you from all your devices!": "Erreur lors de la déconnexion de tous vos appareils !",
|
||||||
|
"Could not find a private conversation!": "Impossible de trouver une conversation privée !",
|
||||||
|
"Could not find related resource!": "La ressource liée n'a pas été trouvée !",
|
||||||
|
"Could not generate new random logo!": "Erreur lors de la génération d'un logo aléatoire !",
|
||||||
|
"Could not get account image settings!": "Erreur lors de la récupération des paramètres de l'image de compte !",
|
||||||
|
"Could not get basic group information!": "Erreur lors de la récupération des informations de base du groupe !",
|
||||||
|
"Could not get conversation information!": "Impossible de récupérer les informations de la conversation !",
|
||||||
|
"Could not get group settings!": "Erreur lors de la récupération des paramètres du groupe !",
|
||||||
|
"Could not get the list of friends of %name% !": "Impossible de récupérer la liste d'amis de %name% !",
|
||||||
|
"Could not get the list of friends of this user !": "Erreur lors de la récupération de la liste d'amis de cet utilisateur !",
|
||||||
|
"Could not get the list of notifications!": "Impossible de récupérer la liste des notifications !",
|
||||||
|
"Could not get the list of posts !": "Impossible de récupérer la liste des posts !",
|
||||||
|
"Could not get user information!": "Impossible de récupérer les informations de l'utilisateur !",
|
||||||
|
"Could not initialize call!": "Erreur lors de l'initialisation de l'appel !",
|
||||||
|
"Could not invite a user!": "Erreur lors de l'envoi de l'invitation pour l'utilisateur !",
|
||||||
|
"Could not leave the conversation!": "Erreur lors du retrait de la conversation !",
|
||||||
|
"Could not load conversation information!": "Erreur lors de la récupération des informations sur la conversation !",
|
||||||
|
"Could not load friendship information!": "Erreur lors de la récupération des informations sur cet ami !",
|
||||||
|
"Could not load general settings!": "Erreur lors du chargement des paramètres généraux !",
|
||||||
|
"Could not load information about the conversation": "Impossible de récupérer les informations sur la conversation",
|
||||||
|
"Could not load information about the group!": "Erreur lors du chargement des informations du groupe !",
|
||||||
|
"Could not load settings!": "Erreur lors du chargement des paramètres",
|
||||||
|
"Could not load the list of groups!": "Erreur lors du chargement de la liste des groupes !",
|
||||||
|
"Could not load the list of members of this conversation!": "Erreur lors de la récupération des membres de cette conversation !",
|
||||||
|
"Could not load the list of members of this group!": "Erreur lors de la récupération de la liste des membres de ce groupe !",
|
||||||
|
"Could not load the list of messages!": "Impossible de charger la liste des messages !",
|
||||||
|
"Could not load the list of unread conversations!": "Erreur lors de la récupération de la liste des conversations non lues !",
|
||||||
|
"Could not load your list of friends!": "Impossible de charger votre liste d'amis !",
|
||||||
|
"Could not load your security questions!": "Erreur lors de la récupération de vos questions de sécurité !",
|
||||||
|
"Could not peform search!": "Erreur lors de l'exécution de la recherche !",
|
||||||
|
"Could not perform search!": "Erreur lors de l'exécution de la recherche !",
|
||||||
|
"Could not pick a PDF!": "Erreur lors du choix d'un PDF",
|
||||||
|
"Could not refresh user information!": "Erreur lors de la récupération des informations de l'utilisateur !",
|
||||||
|
"Could not remove this membership!": "Erreur lors de la suppression de l'inscription !",
|
||||||
|
"Could not remove your membership to this group!": "Erreur lors de la suppresison de votre inscription à ce groupe !",
|
||||||
|
"Could not respond to friendship request!": "Impossible de répondre à la demande d'ami !",
|
||||||
|
"Could not respond to membership request!": "Erreur lors de la réponse à la demande d'inscription !",
|
||||||
|
"Could not respond to your invitation!": "Erreur lors de la réponse à l'invitation !",
|
||||||
|
"Could not retrieve created comment!": "Impossible d'afficher le commentaire !",
|
||||||
|
"Could not retrieve the list of conversations!": "Impossible d'afficher la liste des conversations !",
|
||||||
|
"Could not search virtual directory!": "Erreur lors de la recherche du répertoire virtuel !",
|
||||||
|
"Could not send message!": "Impossible d'envoyer le message !",
|
||||||
|
"Could not send your membership request!": "Erreur lors de l'envoi de votre demande d'inscription !",
|
||||||
|
"Could not send your response to the survey!": "Impossible d'envoyer votre réponse au sondage !",
|
||||||
|
"Could not start streaming!": "Erreur lors du démarrage du flux !",
|
||||||
|
"Could not update account image visibility level!": "Erreur lors de la mise à jour de la visibilité de votre image de compte !",
|
||||||
|
"Could not update comment content!": "Impossible de modifier le contenu du mcommentaire !",
|
||||||
|
"Could not update following status!": "Impossible de modifier le statut de suivi !",
|
||||||
|
"Could not update general settings!": "Erreur lors de la mise à jour des paramètres généraux !",
|
||||||
|
"Could not update group membership!": "Erreur lors de la mise à jour de l'inscription !",
|
||||||
|
"Could not update group settings!": "Erreur lors de la mise à jour des paramètres du groupe !",
|
||||||
|
"Could not update message content!": "Impossible de modifier le contenu du message !",
|
||||||
|
"Could not update password!": "Erreur lors de la mise à jour de votre mot de passe !",
|
||||||
|
"Could not update post content!": "Impossible de modifier le contenu du post !",
|
||||||
|
"Could not update post visibility!": "Impossible de modifier la visibilité du post !",
|
||||||
|
"Could not update security questions!": "Erreur lors de la mise à jour des questions de sécurité !",
|
||||||
|
"Could not update the conversation!": "Impossible de modifier la conversation !",
|
||||||
|
"Could not update your membership!": "Impossible de modifier votre adhésion !",
|
||||||
|
"Could not upload emoji!": "Erreur lors de l'upload de l'émoticon !",
|
||||||
|
"Could not upload new logo!": "Erreur lors de l'envoi du nouveau logo !",
|
||||||
|
"Could not upload your account image!": "Erreur lors de l'envoi de votre image de compte !",
|
||||||
|
"Could not upload your generated account image!": "Erreur lors de l'envoi de votre nouvelle image de compte !",
|
||||||
|
"Could not validate these security answers!": "Erreur lors de la validation de ces réponses de sécurité !",
|
||||||
|
"Could not validate your password reset token! Maybe it has expired now...": "Impossible de vérifier votre clé de changement de mot de passe ! Peut-être a-t-elle expiré...",
|
||||||
|
"Create a conversation": "Créer une conversation",
|
||||||
|
"Create a new choice": "Créer un nouveau choix",
|
||||||
|
"Create a new post...": "Créer un nouveau post...",
|
||||||
|
"Create an account": "Créer un compte",
|
||||||
|
"Create the conversation": "Créer la conversation",
|
||||||
|
"Created on": "Créé le",
|
||||||
|
"Creator": "Créateur",
|
||||||
|
"Crop Photo": "Rogner la photo",
|
||||||
|
"Current account image": "Image de compte actuelle",
|
||||||
|
"Current choices:": "Choix actuels :",
|
||||||
|
"Current level: %level%": "Niveau actuel : %level%",
|
||||||
|
"Current logo": "Logo actuel",
|
||||||
|
"Custom emojis": "Emoticons personnalisés",
|
||||||
|
"Customize your account image": "Personalisez votre image de compte",
|
||||||
|
"Danger zone": "Zone de danger",
|
||||||
|
"Data conservation policy": "Politique de conservation des données",
|
||||||
|
"Debug features": "Fonctionnalités de développement",
|
||||||
|
"Delete": "Supprimer",
|
||||||
|
"Delete account image": "Supprimer l'image de compte",
|
||||||
|
"Delete comment": "Supprimer le commentaire",
|
||||||
|
"Delete conversation": "Supprimer la conversation",
|
||||||
|
"Delete friend": "Supprimer un ami",
|
||||||
|
"Delete group": "Supprimer le groupe",
|
||||||
|
"Delete logo": "Supprimer le logo",
|
||||||
|
"Delete your account": "Supprimer votre compte",
|
||||||
|
"Deprecated application version": "Version obsolète de l'application",
|
||||||
|
"Did not get permission to access microphone!": "Permission d'accéder au microphone refusée !",
|
||||||
|
"Disconnect all your devices": "Déconnecter tous vos appareils",
|
||||||
|
"Disconnect all your devices from Comunic, including the current one. Use this option if one of the device you use for Comunic was stolen.": "Déconnecte tous vos appareils de Comunic, en incluant l'appareil actuel. Nous vous recommandons d'utiliser cette option si vous avez des raisons de penser que l'un des appareils que vous utiliser pour accéder à Comunic a été volé.",
|
||||||
|
"Do you really want to block new choices creation?": "Voulez-vous vraiment bloquer la création de nouveaux choix ?",
|
||||||
|
"Do you really want to block new responses ?": "Voulez-vous vraiment bloquer la création de nouveaux choix ?",
|
||||||
|
"Do you really want to cancel your response to this survey ?": "Voulez-vous vraiment annuler votre réponse au sondage ?",
|
||||||
|
"Do you really want to delete all your notifications?": "Voulez-vous vraiment supprimer toute vos notifications ?",
|
||||||
|
"Do you really want to delete the logo of this group ?": "Voulez-vous vraiment supprimer le logo de ce groupe ?",
|
||||||
|
"Do you really want to delete this comment ?": "Voulez-vous vraiment supprimer ce commentaire ?",
|
||||||
|
"Do you really want to delete this custom emoji ?": "Voulez-vous vraiment supprimer cet émoticon personnalisé ?",
|
||||||
|
"Do you really want to delete this group ? All the posts related to it will be permanently deleted!": "Voulez-vous vraiment supprimer ce groupe ? Tous les posts s'y rapportant seront également supprimés !",
|
||||||
|
"Do you really want to delete this group membership ?": "Voulez-vous vraiment quitter ce groupe ?",
|
||||||
|
"Do you really want to delete this logo?": "Voulez-vous vraiment supprimer ce logo ?",
|
||||||
|
"Do you really want to delete this message ? The operation can not be cancelled !": "Voulez-vous vraiment supprimer ce message ? Cette opération est irréversible !",
|
||||||
|
"Do you really want to delete this post ? The operation can not be reverted !": "Voulez-vous vraiment supprimer ce post ? Cette opération est irréversible !",
|
||||||
|
"Do you really want to delete your account image ?": "Voulez-vous vraiment supprimer votre image de compte ?",
|
||||||
|
"Do you really want to delete your account? This operation CAN NOT be reverted!": "Voulez-vous vraiment supprimer votre compte ? Cette opération NE PEUT PAS être annulée !",
|
||||||
|
"Do you really want to disconnect all your devices from Comunic ?": "Voulez-vous vraiment déconnecter tous vos appareils de Comunic ?",
|
||||||
|
"Do you really want to leave this call ?": "Voulez-vous vraiment quitter cet appel ?",
|
||||||
|
"Do you really want to leave this conversation ?": "Voulez-vous vraiment quitter cette conversation ?",
|
||||||
|
"Do you really want to leave this conversation ? As you are its last admin, it will be completely deleted!": "Voulez-vous vraiment quitter cette conversation ? Comme vous êtes son dernier administrateur, celle-ci sera complètement supprimée !",
|
||||||
|
"Do you really want to reject this friendship request?": "Voulez-vous vraiment rejeter cette demande d'amis ?",
|
||||||
|
"Do you really want to reject this invitation?": "Voulez-vous vraiment refuser cette invitation ?",
|
||||||
|
"Do you really want to remove this conversation from your list of conversations ? If you are the owner of this conversation, it will be completely deleted!": "Voulez-vous vraiment supprimer la conversation de votre liste ? Si vous êtes le créateur de cette conversation, elle sera définitivement supprimée !",
|
||||||
|
"Do you really want to remove this membership ?": "Voulez-vous vraiment supprimer cette inscription ?",
|
||||||
|
"Do you really want to sign out from the application ?": "Voulez-vous vraiment vous déconnecter de l'application ?",
|
||||||
|
"Do you want to unselected currently selected image ?": "Voulez-vous désélectionner l'image ?",
|
||||||
|
"Done": "Terminé",
|
||||||
|
"Download update outside Play Store": "Télécharger la mise hors du Play Store",
|
||||||
|
"Email address": "Adresse e-mail",
|
||||||
|
"Email address...": "Adresse mail...",
|
||||||
|
"Enable dark theme": "Activer le thème sombre",
|
||||||
|
"Error": "Erreur",
|
||||||
|
"Error while creating your account": "Une erreur s'est produite lors de la création de votre compte.",
|
||||||
|
"Error while pausing playback!": "Erreur lors de la pause de la lecture !",
|
||||||
|
"Error while playing record!": "Erreur lors de la lecture de l'enregistrement !",
|
||||||
|
"Error while processing new signal!": "Erreur lors du traitement d'un signal !",
|
||||||
|
"Error while recording!": "Erreur lors de l'enregistrement !",
|
||||||
|
"Error while resuming playback!": "Erreur lors de la reprise de la lecture !",
|
||||||
|
"Error while stopping playback!": "Erreur lors de l'arrêt de la lecture !",
|
||||||
|
"Everyone": "Tout le monde",
|
||||||
|
"Everyone can choose to join the group without moderator approval": "Tout le monde peut rejoindre le groupe, sans l'approbation d'un modérateur",
|
||||||
|
"Everyone can request a membership, but a moderator review the request": "Tout le monde peut demander à rejoindre le groupe, mais un modérateur doit accepter les demandes",
|
||||||
|
"Failed to add member to conversation!": "Echec de l'ajout d'un membre à la conversation !",
|
||||||
|
"Failed to change conversation logo !": "Erreur lors du changement de logo pour la conversation !",
|
||||||
|
"Failed to choose an image!": "Erreur lors du choix d'une image !",
|
||||||
|
"Failed to execute image cropper!": "Echec de l'exécution du rogneur d'image !",
|
||||||
|
"Failed to initialize audio player!": "Echec de l'initialisation du lecteur audio !",
|
||||||
|
"Failed to initialize video!": "Erreur lors de l'initialisation de la vidéo !",
|
||||||
|
"Failed to load conversation settings!": "Echec du chargement des paramètres de la conversation !",
|
||||||
|
"Failed to load message information!": "Echec du chargement des informations du message !",
|
||||||
|
"Failed to load privacy settings!": "Erreur lors du chargement des paramètres de vie privée !",
|
||||||
|
"Failed to pick an image for the post!": "Echec de la sélection d'une image pour le post !",
|
||||||
|
"Failed to pick an image!": "Echec de la sélection d'une image !",
|
||||||
|
"Failed to remove conversation logo!": "Erreur lors de la suppression du logo de la conversation !",
|
||||||
|
"Failed to remove member!": "Echec de la suppression d'un membre !",
|
||||||
|
"Failed to send a file!": "Erreur lors de l'envoi d'un fichier !",
|
||||||
|
"Failed to start recording!": "Erreur lors du lancement de l'enregistrement !",
|
||||||
|
"Failed to toggle admin status of user!": "Echec du changement du status administrateur d'un membre !",
|
||||||
|
"Failed to update conversation settings!": "Echec de la mise à jour des paramètres de la conversation !",
|
||||||
|
"Failed to update data conservation policy!": "Echec de la mise à jour des paramètres de vie privée !",
|
||||||
|
"Failed to upload new account image!": "Echec de l'envoi de la nouvelle image de compte !",
|
||||||
|
"First name": "Prénom",
|
||||||
|
"Follow": "Suivre",
|
||||||
|
"Follow conversation": "Suivre la conversation",
|
||||||
|
"Following": "Suivi",
|
||||||
|
"Force mobile mode": "Forcer l'utilisation du mode mobile",
|
||||||
|
"Force the smartphone mode of the application to be used, even when tablet mode could be used.": "Forcer l'utilisation du mode smartphone de l'application, même lorsque le mode tablette est disponible.",
|
||||||
|
"Form can not be submitted at this point!": "Impossible de soumettre le formulaire à ce stade !",
|
||||||
|
"Free social network that respect your privacy": "Réseau social libre qui respecte votre vie privée",
|
||||||
|
"Friends": "Amis",
|
||||||
|
"Friends of %name%": "Amis de %name%",
|
||||||
|
"Friends only": "Amis seulement",
|
||||||
|
"General": "Général",
|
||||||
|
"General information": "Informations générales",
|
||||||
|
"General settings": "Paramètres généraux",
|
||||||
|
"Generate a new random logo": "Générer un logo aléatoire",
|
||||||
|
"Generate a random account image": "Générer une image de compte aléatoire",
|
||||||
|
"Go to the Play Store": "Accéder au Play Store",
|
||||||
|
"Group": "Groupe",
|
||||||
|
"Group ID": "Identifiant du gorupe",
|
||||||
|
"Group URL (optional)": "URL du groupe (optionnelle)",
|
||||||
|
"Group description (optional)": "Description du groupe (optionnelle)",
|
||||||
|
"Group information & public posts are available to everyone.": "Les informations du groupe ainsi que ses posts public sont accessibles à tous",
|
||||||
|
"Group logo": "Logo du groupe",
|
||||||
|
"Group members": "Membres du groupe",
|
||||||
|
"Group members only": "Membres du groupe seulement",
|
||||||
|
"Group name": "Nom du groupe",
|
||||||
|
"Group registration level": "Inscription au groupe",
|
||||||
|
"Group settings": "Paramètres du groupe",
|
||||||
|
"Group visibility": "Visibilité du groupe",
|
||||||
|
"Groups": "Groupes",
|
||||||
|
"Here are your options to reset your account:": "Voici les options à votre disposition pour réinitialiser votre compte :",
|
||||||
|
"Here you can make actions to protect your privacy": "Agissez pour protéger votre vie privée",
|
||||||
|
"I have read and accepted the Terms Of Service.": "J'ai lu et accepté les Conditions d'utilisation.",
|
||||||
|
"Image": "Image",
|
||||||
|
"Image gallery": "Galerie",
|
||||||
|
"Input YouTube URL": "Entrez l'URL de la vidéo YouTube",
|
||||||
|
"Invalid URL!": "URL invalide !",
|
||||||
|
"Invalid YouTube link!": "Lien YouTube invalide !",
|
||||||
|
"Invalid credentials!": "Identifiants invalides !",
|
||||||
|
"Invalid email address!": "Adresse e-mail invalide !",
|
||||||
|
"Invalid first name!": "Prénom invalide !",
|
||||||
|
"Invalid last name!": "Nom invalide !",
|
||||||
|
"Invalid password!": "Mot de passe invalide !",
|
||||||
|
"Invalid shortcut!": "Raccourcis invalide !",
|
||||||
|
"Invalid value!": "Valeur invalide !",
|
||||||
|
"Invited": "Invité",
|
||||||
|
"Last name": "Nom",
|
||||||
|
"Learn more about us": "En savoir plus sur nous",
|
||||||
|
"Leave": "Quitter",
|
||||||
|
"Let us ask you one last time. Do you really want to delete your account? If you decide to do so, your data will be permanently removed from our servers, so we will not be able to recover your account. If you decide to proceed, the deletion process will start immediatly and you will automatically get disconnected from your account.": "Laissez-nous vous demander une dernière fois. Voulez-vous vraiment supprimer votre compte ? Si vous décidez de continuer, les données liées à votre compte vont être supprimées de manière permanente de nos serveurs, et nous ne seront pas en mesure de les restaurer. Si vous décidez de poursuivre, le processus de supprimer vas débuter immédiatement et vous serez automatiquement déconnecté de votre compte.",
|
||||||
|
"Like": "J'aime",
|
||||||
|
"Loading": "Chargement",
|
||||||
|
"Loading...": "Chargement...",
|
||||||
|
"Login": "Connexion",
|
||||||
|
"Main account information": "Informations principales du compte",
|
||||||
|
"Make your friends list public": "Rendre votre liste d'amis publique",
|
||||||
|
"Manage local application settings": "Paramètres locaux de l'application",
|
||||||
|
"Manage security options of your account": "Accédez aux options de sécurité pour votre compte",
|
||||||
|
"Me only": "Moi seulement",
|
||||||
|
"Member": "Membre",
|
||||||
|
"Member for %t%": "Membre depuis %t%",
|
||||||
|
"Members": "Membres",
|
||||||
|
"Membership": "Inscription",
|
||||||
|
"Menu": "Menu",
|
||||||
|
"Message not seen yet": "Message non vu",
|
||||||
|
"Message rejected by the server!": "Message rejeté par le serveur !",
|
||||||
|
"Message seen": "Message vu",
|
||||||
|
"Message statistics": "Statistiques du message",
|
||||||
|
"Moderated registration": "Inscription modérée",
|
||||||
|
"Moderator": "Modérateur",
|
||||||
|
"Moderators only": "Modérateurs uniquement",
|
||||||
|
"My Page": "Ma page",
|
||||||
|
"My friends": "Mes amis",
|
||||||
|
"My friends only": "Mes amis uniquement",
|
||||||
|
"Name of the group": "Nom du groupe",
|
||||||
|
"Name of the group to create": "Nom du groupe à créer",
|
||||||
|
"Never": "Jamais",
|
||||||
|
"New choice": "Nouveau choix",
|
||||||
|
"New choice...": "Nouveau choix...",
|
||||||
|
"New comment...": "Nouveau commentaire...",
|
||||||
|
"New content...": "Nouveau contenu...",
|
||||||
|
"New content:": "Nouveau contenu :",
|
||||||
|
"New file": "Nouveau fichier",
|
||||||
|
"New membership level": "Nouveau niveau d'appartenance au groupe",
|
||||||
|
"New message": "Nouveau message",
|
||||||
|
"New message...": "Nouveau message...",
|
||||||
|
"New password": "Nouveau mot de passe",
|
||||||
|
"New survey": "Nouveau sondage",
|
||||||
|
"Newest": "Plus récent",
|
||||||
|
"Night mode": "Thème sombre",
|
||||||
|
"No account image yet...": "Pas encore d'image de compte...",
|
||||||
|
"No choice yet.": "Aucun choix pour le moment.",
|
||||||
|
"No response yet to this survey.": "Pas encore de réponse à ce sondage",
|
||||||
|
"Note": "Note",
|
||||||
|
"Note: Your two questions and answers MUST be completed in order to be able to recover your account using your security questions!": "Note : Vos deux questions de sécurité DOIVENT être complétées pour que vous puissiez récupérer l'accès à votre compte depuis vos questions de sécurité !",
|
||||||
|
"Notifications": "Notifications",
|
||||||
|
"OK": "OK",
|
||||||
|
"Ok": "Ok",
|
||||||
|
"Ongoing call": "Appel en cours",
|
||||||
|
"Online": "Connecté",
|
||||||
|
"Only moderators and administrators of the group can create posts on it": "Seuls les modérateurs et les administrateurs du groupe peuvent créer des posts",
|
||||||
|
"Open": "Ouvert",
|
||||||
|
"Open group": "Groupe ouvert",
|
||||||
|
"Open in full screen": "Ouvrir en plein écran",
|
||||||
|
"Open registration": "Inscription ouverte",
|
||||||
|
"Owner": "Propriétaire",
|
||||||
|
"PDF": "PDF",
|
||||||
|
"Page visibility": "Visibilité de la page",
|
||||||
|
"Password": "Mot de passe",
|
||||||
|
"Password forgotten": "Mot de passe oublié",
|
||||||
|
"Password required": "Mot de passe requis",
|
||||||
|
"Permanently delete your account and all data related to it.": "Supprimer de manière permanente votre compte et toute les données qui y sont rattachées",
|
||||||
|
"Personal website URL (optional)": "Site web personnel (optionnel)",
|
||||||
|
"Playback paused...": "Lecture en pause",
|
||||||
|
"Playing...": "Lecture...",
|
||||||
|
"Please answer now your security questions:": "Veuillez répondre à vos questions de sécurité :",
|
||||||
|
"Please choose new account image visibility level:": "Veuillez choisir un nouveau niveau de visibilité pour votre image de compte :",
|
||||||
|
"Please enter message content: ": "Veuillez entrer le contenu du message :",
|
||||||
|
"Please enter new message content:": "Veuillez entrer le contenu du nouveau message :",
|
||||||
|
"Please enter your email address to reset your password:": "Veuillez entrer votre adresse mail pour changer votre mot de passe :",
|
||||||
|
"Please sign into your Comunic account: ": "Veuillez vous connecter à votre compte Comunic :",
|
||||||
|
"Please specify the new choice for the survey": "Veuillez spécifier le nouveau choix pour ce sondage",
|
||||||
|
"Post": "Post",
|
||||||
|
"Post content": "Contenu du post",
|
||||||
|
"Posts creation level": "Création de posts",
|
||||||
|
"Privacy": "Vie privée",
|
||||||
|
"Privacy settings": "Vie privée",
|
||||||
|
"Private": "Privé",
|
||||||
|
"Private conversation": "Conversation privée",
|
||||||
|
"Private group": "Groupe privé",
|
||||||
|
"Private, accessible only to your friends": "Privé, accessible uniquement par vous et vos amis",
|
||||||
|
"Public": "Public",
|
||||||
|
"Public note (optional)": "Note publique (optionnelle)",
|
||||||
|
"Public, accessible to all Comunic members": "Publique, accessible par tous les membres de Comunic",
|
||||||
|
"Question": "Question",
|
||||||
|
"Question 1": "Question 1",
|
||||||
|
"Question 2": "Question 2",
|
||||||
|
"Ready": "Prêt",
|
||||||
|
"Record audio": "Faire un enregistrement audio",
|
||||||
|
"Recording...": "Enregistrement...",
|
||||||
|
"Reject": "Rejeter",
|
||||||
|
"Reject request": "Rejeter la demande",
|
||||||
|
"Remove": "Supprimer",
|
||||||
|
"Remove selected image": "Supprimer l'image sélectionnée",
|
||||||
|
"Replace image": "Remplacer l'image",
|
||||||
|
"Request membership": "Demander de rejoindre le groupe",
|
||||||
|
"Requested": "En attente",
|
||||||
|
"Respond to survey": "Répondre au sondage",
|
||||||
|
"Retry": "Ré-essayer",
|
||||||
|
"Search": "Recherche",
|
||||||
|
"Search a user, a group...": "Rechercher un utilisateur, un groupe...",
|
||||||
|
"Search user...": "Rechercher un utilisateur...",
|
||||||
|
"Search...": "Rechercher...",
|
||||||
|
"Secrete group": "Groupe secret",
|
||||||
|
"Security": "Sécurité",
|
||||||
|
"Select new post visibility level": "Sélectionner la nouvelle visibilité du post",
|
||||||
|
"Send": "Envoyer",
|
||||||
|
"Send a message": "Envoyer un message",
|
||||||
|
"Send request": "Envoyer la demande",
|
||||||
|
"Send us an email to ask for help": "Envoyez-nous un mail pour demander notre aide",
|
||||||
|
"Set your own emoticon shorcuts": "Définissez vos propres raccourcis d'émoticons",
|
||||||
|
"Settings": "Paramètres",
|
||||||
|
"Shortcut": "Raccourcis",
|
||||||
|
"Show more comments": "Afficher plus de commentaires",
|
||||||
|
"Show performances overlay": "Afficher les informations sur les performances",
|
||||||
|
"Sign in": "Connexion",
|
||||||
|
"Sign out": "Déconnexion",
|
||||||
|
"Specified email address was not found!": "L'adresse mail spécifiée n'a pas été trouvée !",
|
||||||
|
"Specify URL": "Spécifier l'URL",
|
||||||
|
"Statistics": "Statistiques",
|
||||||
|
"Stop streaming": "Arrêter de partager ma vidéo & mon audio",
|
||||||
|
"Submit": "Valider",
|
||||||
|
"Switch camera": "Changer de caméra",
|
||||||
|
"Take a picture": "Prendre une photo",
|
||||||
|
"Take a video": "Prendre une vidéo",
|
||||||
|
"The group is accessible to accepted members only.": "Le groupe n'est accessible qu'à ses membres",
|
||||||
|
"The group is visible only to invited members.": "Le groupe n'apparaît qu'à ses membres, et aux personnes invitées à le rejoindre.",
|
||||||
|
"The only way to join the group is to be invited by a moderator": "Seul un modérateur peut inviter quelqu'un à rejoindre le groupe",
|
||||||
|
"The password and its confirmation do not match!": "La confirmation ne correspond pas au mot de passe !",
|
||||||
|
"The post has been successfully created!": "Le post a été créé avec succès !",
|
||||||
|
"There is no message yet in this conversation.": "Il y n'a pas encore de message dans cette conversation.",
|
||||||
|
"There is no post to display here yet.": "Il n'y a pas encore de post à afficher.",
|
||||||
|
"This account is private.": "Ce compte est privé.",
|
||||||
|
"This file could not be sent: it is too big! (Max allowed size: %1%)": "Ce fichier ne peut pas être envoyé : il est trop lourd ! (Taille maximale autorisée: %1%)",
|
||||||
|
"This kind of notification is not supported yet by this application.": "Ce type de notification n'est pas encore supportée par l'application.",
|
||||||
|
"This password is not the same as the other one!": "Ce mot de passe est différent de l'autre",
|
||||||
|
"This version of the Comunic application is deprecated. You might still be able to use it, but some features may not work. We recommend you to update to the latest version of the application.": "Cette version de l'application Comunic est obsolète. Vous pouvez continuer à l'utiliser, mais certaines fonctionalités pourront ne plus fonctionner. Nous vous recommandons d'installer la dernière version de l'applicatioon.",
|
||||||
|
"This virtual directory is invalid / unvailable !": "Ce répertoire virtuel est invalide / indisponible !",
|
||||||
|
"Toggle admin status": "Changer le status d'admin",
|
||||||
|
"Too many accounts have been created from this IP address for now. Please try again later.": "Trop de comptes ont été créés avec cette addresse IP pour l'instant. Veuillez ré-essayer plus tard.",
|
||||||
|
"Too many unsuccessfull login attempts! Please try again later...": "Trop de tentatives de connexion ont échoué. Veuillez ré-essayer plus tard...",
|
||||||
|
"Try again": "Essayer à nouveau",
|
||||||
|
"Unsafe value!": "Valeur non sûre !",
|
||||||
|
"Update": "Modifier",
|
||||||
|
"Update a conversation": "Modifier une conversation",
|
||||||
|
"Update comment content": "Modifier le contenu du commentaire",
|
||||||
|
"Update content": "Modifier le contenu",
|
||||||
|
"Update conversation": "Mise à jour d'une conversation",
|
||||||
|
"Update message": "Modifier un message",
|
||||||
|
"Update post content": "Modifier le contenu du post",
|
||||||
|
"Update security questions": "Mise à jour des questions de sécurité",
|
||||||
|
"Update the conversation": "Modifier la conversation",
|
||||||
|
"Upload a new logo": "Envoyer un nouveau logo",
|
||||||
|
"Upload an account image": "Envoyer une nouvelle image de compte",
|
||||||
|
"Upload new account image": "Changer l'image de compte",
|
||||||
|
"Use the old application anyway": "Utiliser l'ancienne version",
|
||||||
|
"User ID": "Numéro d'utilisateur",
|
||||||
|
"Version %version% - Build %build%": "Version %version% - Build %build%",
|
||||||
|
"Virtual directory": "Répertoire virtuel",
|
||||||
|
"Virtual directory (optional)": "Dossier virtuel (optionnel)",
|
||||||
|
"Visitor": "Visiteur",
|
||||||
|
"Website": "Site web",
|
||||||
|
"You can choose a new password.": "Vous pouvez choisir un nouveau mot de passe.",
|
||||||
|
"You can reach us at contact@communiquons.org": "Vous pouvez nous contacter à l'adresse contact@communiquons.org",
|
||||||
|
"You can use this virtual directory.": "Vous pouvez utiliser ce répertoire virtuel.",
|
||||||
|
"You do not have any notification now.": "Vous n'avez pas de notification pour l'intant.",
|
||||||
|
"You do not have any unread conversation yet...": "Vous n'avez aucune conversation non lue pour le moment...",
|
||||||
|
"You must accept the Terms Of Service to continue.": "Vous devez accepter les Conditions d'utilisation pour continuer.",
|
||||||
|
"You security questions have been successfully updated!": "Vos questions de sécurité ont été mises avec succès !",
|
||||||
|
"You will need to restart the application to apply changes": "Vous aurez besoin de redémarrer l'application pour appliquer les changements",
|
||||||
|
"YouTube movie": "Vidéo YouTube",
|
||||||
|
"Your account has been successfully created. You can now login to start to use it.": "Votre compte a été créé avec succès. Vous pouvez à présent vous connecter pour commencer à l'utiliser !",
|
||||||
|
"Your account image is visible by everyone, including users external to Comunic.": "Votre image de compte est visible par tout le monde, ainsi que les personnes non connectées.",
|
||||||
|
"Your account image is visible only by your friends.": "Votre image de compte n'est visible que par vos amis",
|
||||||
|
"Your account image is visible only to connected Comunic users.": "Votre image de compte n'est accessible qu'aux personnes connectées.",
|
||||||
|
"Your current password": "Mot de passe actuel",
|
||||||
|
"Your friends list": "Votre liste d'amis",
|
||||||
|
"Your new password": "Votre nouveau mot de passe",
|
||||||
|
"Your page settings": "Paramètres de votre page",
|
||||||
|
"Your password has been successfully changed!": "Votre mot de passe a été changé avec succès !",
|
||||||
|
"Your password must be composed of at least %num% characters!": "Votre mot de passe doit être composé d'au moins %num% caractères !",
|
||||||
|
"Your password must contains characters of at least %num% of the following categories : %upper% upper case letter, %lower% lowercase letter, %digit% digit, %special% special character.": "Votre mot de passe doit contenir des caractères d'au moins %num% des catégories suivants : %upper% lettre majuscule, %lower% lettre minuscule, %digit% chiffre, %special% caractères spéciaux.",
|
||||||
|
"Your password must not contains part of your email address!": "Votre mot de passe ne doit pas contenir des parties de votre adresse mail !",
|
||||||
|
"Your password must not contains your first name!": "Votre mot de passe ne doit pas contenir votre prénom !",
|
||||||
|
"Your password must not contains your last name!": "Votre mot de passe ne doit pas contenir votre nom !",
|
||||||
|
"Your response: %response%": "Votre réponse : %response%",
|
||||||
|
"Your security questions can be used to recover an access to your account when you loose your password...": "Vos questions de sécurité peuvent être utilisées pour récupérer l'accès à votre compte lorsque vous perdez votre mot de passe...",
|
||||||
|
"accepted his invitation to join the group": "a accepté son invitation à rejoindre le groupe",
|
||||||
|
"accepted you request to join the group": "a accepté votre demande à rejoindre le groupe",
|
||||||
|
"accepted your friendship request.": "a accepté cotre demande d'ami",
|
||||||
|
"cancel": "annuler",
|
||||||
|
"created a new post": "a créé un nouveau post",
|
||||||
|
"delete": "supprimer",
|
||||||
|
"https://www.youtube.com/watch/?v=": "https://www.youtube.com/watch/?v=",
|
||||||
|
"invited you to join the group": "vous a invité à rejoindre le groupe",
|
||||||
|
"on %user_name%'s page": "sur la page de %user_name%",
|
||||||
|
"on his / her page": "sur sa page",
|
||||||
|
"on the group %group%.": "sur le groupe %group%",
|
||||||
|
"posted a comment": "a posté un commentaire",
|
||||||
|
"rejected his invitation to join the group": "a rejeté son invitation à rejoindre le groupe",
|
||||||
|
"rejected your friendship request.": "a rejeté votre demande d'ami",
|
||||||
|
"rejected your request to join the group": "a rejeté votre demande à rejoindre le groupe",
|
||||||
|
"sent a request to join the group": "a envoyé une demande à rejoindre le groupe",
|
||||||
|
"sent you a friendship request.": "vous a envoyé une demande d'ami"
|
||||||
|
}
|
6
lib/constants.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/// Comunic mobile constants
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
/// Data serialisation directory
|
||||||
|
const SERIALIZATION_DIRECTORY = "serialization";
|
@ -2,13 +2,4 @@
|
|||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
enum PostKind {
|
enum PostKind { TEXT, IMAGE, WEB_LINK, PDF, COUNTDOWN, SURVEY, YOUTUBE }
|
||||||
TEXT,
|
|
||||||
IMAGE,
|
|
||||||
WEB_LINK,
|
|
||||||
PDF,
|
|
||||||
MOVIE,
|
|
||||||
COUNTDOWN,
|
|
||||||
SURVEY,
|
|
||||||
YOUTUBE
|
|
||||||
}
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:comunic/helpers/api_helper.dart';
|
import 'package:comunic/helpers/api_helper.dart';
|
||||||
import 'package:comunic/helpers/preferences_helper.dart';
|
import 'package:comunic/helpers/preferences_helper.dart';
|
||||||
|
import 'package:comunic/helpers/websocket_helper.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
import 'package:comunic/models/authentication_details.dart';
|
import 'package:comunic/models/authentication_details.dart';
|
||||||
import 'package:comunic/models/login_tokens.dart';
|
import 'package:comunic/models/new_account.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:comunic/models/res_check_password_reset_token.dart';
|
||||||
|
|
||||||
/// Account helper
|
/// Account helper
|
||||||
///
|
///
|
||||||
@ -16,9 +17,14 @@ enum AuthResult {
|
|||||||
INVALID_CREDENTIALS
|
INVALID_CREDENTIALS
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountHelper {
|
enum CreateAccountResult {
|
||||||
static const _USER_ID_PREFERENCE_NAME = "user_id";
|
SUCCESS,
|
||||||
|
ERROR_TOO_MANY_REQUESTS,
|
||||||
|
ERROR_EXISTING_EMAIL,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountHelper {
|
||||||
// Current user ID
|
// Current user ID
|
||||||
static int _currentUserID = -1;
|
static int _currentUserID = -1;
|
||||||
|
|
||||||
@ -27,7 +33,7 @@ class AccountHelper {
|
|||||||
/// Warning : This method MUST BE CALLED AT LEAST ONCE AFTER APP START !!!
|
/// Warning : This method MUST BE CALLED AT LEAST ONCE AFTER APP START !!!
|
||||||
Future<bool> signedIn() async {
|
Future<bool> signedIn() async {
|
||||||
bool signedIn =
|
bool signedIn =
|
||||||
(await PreferencesHelper.getInstance()).getLoginTokens() != null;
|
(await PreferencesHelper.getInstance()).getLoginToken() != null;
|
||||||
|
|
||||||
// Load current user ID for later use
|
// Load current user ID for later use
|
||||||
if (signedIn && _currentUserID == -1) await _loadCurrentUserID();
|
if (signedIn && _currentUserID == -1) await _loadCurrentUserID();
|
||||||
@ -38,8 +44,8 @@ class AccountHelper {
|
|||||||
/// Sign in user
|
/// Sign in user
|
||||||
Future<AuthResult> signIn(AuthenticationDetails auth) async {
|
Future<AuthResult> signIn(AuthenticationDetails auth) async {
|
||||||
final request = APIRequest(uri: "account/login");
|
final request = APIRequest(uri: "account/login");
|
||||||
request.addString("userMail", auth.email);
|
request.addString("mail", auth.email);
|
||||||
request.addString("userPassword", auth.password);
|
request.addString("password", auth.password);
|
||||||
|
|
||||||
final response = await APIHelper().exec(request);
|
final response = await APIHelper().exec(request);
|
||||||
|
|
||||||
@ -50,10 +56,9 @@ class AccountHelper {
|
|||||||
return AuthResult.TOO_MANY_ATTEMPTS;
|
return AuthResult.TOO_MANY_ATTEMPTS;
|
||||||
else if (response.code != 200) return AuthResult.NETWORK_ERROR;
|
else if (response.code != 200) return AuthResult.NETWORK_ERROR;
|
||||||
|
|
||||||
// Save login tokens
|
// Save login token
|
||||||
final tokensObj = response.getObject()["tokens"];
|
|
||||||
await (await PreferencesHelper.getInstance())
|
await (await PreferencesHelper.getInstance())
|
||||||
.setLoginTokens(LoginTokens(tokensObj["token1"], tokensObj["token2"]));
|
.setLoginToken(response.getObject()["token"]);
|
||||||
|
|
||||||
// Get current user ID
|
// Get current user ID
|
||||||
final userID = await _downloadCurrentUserID();
|
final userID = await _downloadCurrentUserID();
|
||||||
@ -63,8 +68,8 @@ class AccountHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save current user ID
|
// Save current user ID
|
||||||
final preferences = await SharedPreferences.getInstance();
|
final preferences = await PreferencesHelper.getInstance();
|
||||||
await preferences.setInt(_USER_ID_PREFERENCE_NAME, userID);
|
await preferences.setInt(PreferencesKeyList.USER_ID, userID);
|
||||||
_currentUserID = userID;
|
_currentUserID = userID;
|
||||||
|
|
||||||
return AuthResult.SUCCESS;
|
return AuthResult.SUCCESS;
|
||||||
@ -72,16 +77,122 @@ class AccountHelper {
|
|||||||
|
|
||||||
/// Sign out user
|
/// Sign out user
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
await (await PreferencesHelper.getInstance()).setLoginTokens(null);
|
await APIRequest.withLogin("account/logout").exec();
|
||||||
|
|
||||||
|
final preferencesHelper = await PreferencesHelper.getInstance();
|
||||||
|
await preferencesHelper.setLoginToken(null);
|
||||||
|
await preferencesHelper.setInt(PreferencesKeyList.USER_ID, -1);
|
||||||
_currentUserID = 0;
|
_currentUserID = 0;
|
||||||
|
|
||||||
|
// Close current web socket
|
||||||
|
WebSocketHelper.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new user account
|
||||||
|
Future<CreateAccountResult> createAccount(NewAccount info) async {
|
||||||
|
final response = await APIRequest(
|
||||||
|
uri: "account/create",
|
||||||
|
needLogin: false,
|
||||||
|
args: {
|
||||||
|
"firstName": info.firstName,
|
||||||
|
"lastName": info.lastName,
|
||||||
|
"emailAddress": info.email,
|
||||||
|
"password": info.password,
|
||||||
|
},
|
||||||
|
).exec();
|
||||||
|
|
||||||
|
switch (response.code) {
|
||||||
|
case 200:
|
||||||
|
return CreateAccountResult.SUCCESS;
|
||||||
|
|
||||||
|
case 409:
|
||||||
|
return CreateAccountResult.ERROR_EXISTING_EMAIL;
|
||||||
|
|
||||||
|
case 429:
|
||||||
|
return CreateAccountResult.ERROR_TOO_MANY_REQUESTS;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return CreateAccountResult.ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check out whether a given email address exists or not
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<bool> existsMailAccount(String email) async =>
|
||||||
|
(await APIRequest.withoutLogin("account/exists_email")
|
||||||
|
.addString("email", email)
|
||||||
|
.execWithThrow())
|
||||||
|
.getObject()["exists"];
|
||||||
|
|
||||||
|
/// Get current user email address
|
||||||
|
static Future<String> getCurrentAccountEmailAddress() async =>
|
||||||
|
(await APIRequest.withLogin("account/mail")
|
||||||
|
.execWithThrowGetObject())["mail"];
|
||||||
|
|
||||||
|
/// Check out whether security questions have been set for an account or not
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<bool> hasSecurityQuestions(String email) async =>
|
||||||
|
(await APIRequest.withoutLogin("account/has_security_questions")
|
||||||
|
.addString("email", email)
|
||||||
|
.execWithThrow())
|
||||||
|
.getObject()["defined"];
|
||||||
|
|
||||||
|
/// Get the security questions of the user
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<List<String>> getSecurityQuestions(String email) async =>
|
||||||
|
((await APIRequest.withoutLogin("account/get_security_questions")
|
||||||
|
.addString("email", email)
|
||||||
|
.execWithThrow())
|
||||||
|
.getObject()["questions"])
|
||||||
|
.cast<String>();
|
||||||
|
|
||||||
|
/// Validate given security answers
|
||||||
|
///
|
||||||
|
/// Throws an [Exception] in case of failure
|
||||||
|
///
|
||||||
|
/// Returns a password reset token in case of success
|
||||||
|
static Future<String> checkAnswers(
|
||||||
|
String email, List<String> answers) async =>
|
||||||
|
(await APIRequest.withoutLogin("account/check_security_answers")
|
||||||
|
.addString("email", email)
|
||||||
|
.addString("answers",
|
||||||
|
answers.map((f) => Uri.encodeComponent(f)).join("&"))
|
||||||
|
.execWithThrow())
|
||||||
|
.getObject()["reset_token"];
|
||||||
|
|
||||||
|
/// Check a password reset token
|
||||||
|
///
|
||||||
|
/// Throws in case failure
|
||||||
|
static Future<ResCheckPasswordToken> validatePasswordResetToken(
|
||||||
|
String token) async {
|
||||||
|
final response =
|
||||||
|
await APIRequest.withoutLogin("account/check_password_reset_token")
|
||||||
|
.addString("token", token)
|
||||||
|
.execWithThrowGetObject();
|
||||||
|
|
||||||
|
return ResCheckPasswordToken(
|
||||||
|
firstName: response["first_name"],
|
||||||
|
lastName: response["last_name"],
|
||||||
|
email: response["mail"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change account password using password reset token
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
|
static Future<void> changeAccountPassword(
|
||||||
|
String token, String password) async =>
|
||||||
|
await APIRequest.withoutLogin("account/reset_user_passwd")
|
||||||
|
.addString("token", token)
|
||||||
|
.addString("password", password)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
/// Get current user ID from the server
|
/// Get current user ID from the server
|
||||||
Future<int> _downloadCurrentUserID() async {
|
Future<int> _downloadCurrentUserID() async {
|
||||||
final response = await APIRequest(
|
final response = await APIRequest.withLogin("account/id").exec();
|
||||||
uri: "user/getCurrentUserID",
|
|
||||||
needLogin: true,
|
|
||||||
).exec();
|
|
||||||
|
|
||||||
if (response.code != 200) return null;
|
if (response.code != 200) return null;
|
||||||
|
|
||||||
@ -90,13 +201,33 @@ class AccountHelper {
|
|||||||
|
|
||||||
/// Get the ID of the currently signed in user
|
/// Get the ID of the currently signed in user
|
||||||
Future<void> _loadCurrentUserID() async {
|
Future<void> _loadCurrentUserID() async {
|
||||||
final preferences = await SharedPreferences.getInstance();
|
final preferences = await PreferencesHelper.getInstance();
|
||||||
_currentUserID = preferences.getInt(_USER_ID_PREFERENCE_NAME);
|
_currentUserID = preferences.getInt(PreferencesKeyList.USER_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if current user ID is loaded or not
|
||||||
|
static bool get isUserIDLoaded => _currentUserID > 0;
|
||||||
|
|
||||||
/// Get the ID of the currently signed in user
|
/// Get the ID of the currently signed in user
|
||||||
static int getCurrentUserID() {
|
static int getCurrentUserID() {
|
||||||
if (_currentUserID == -1) throw "Current user ID has not been loaded yet!";
|
if (_currentUserID == -1) throw "Current user ID has not been loaded yet!";
|
||||||
return _currentUserID;
|
return _currentUserID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disconnect all the devices of the current user
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> disconnectAllDevices() async {
|
||||||
|
await APIRequest(uri: "account/disconnect_all_devices", needLogin: true)
|
||||||
|
.execWithThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove permanently a user account
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> deleteAccount(String password) async {
|
||||||
|
await APIRequest(uri: "account/delete", needLogin: true)
|
||||||
|
.addString("password", password)
|
||||||
|
.execWithThrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:comunic/helpers/events_helper.dart';
|
||||||
import 'package:comunic/helpers/preferences_helper.dart';
|
import 'package:comunic/helpers/preferences_helper.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
import 'package:comunic/models/api_response.dart';
|
import 'package:comunic/models/api_response.dart';
|
||||||
@ -18,15 +19,13 @@ class APIHelper {
|
|||||||
Future<APIResponse> exec(APIRequest request, {bool multipart = false}) async {
|
Future<APIResponse> exec(APIRequest request, {bool multipart = false}) async {
|
||||||
try {
|
try {
|
||||||
//Add API tokens
|
//Add API tokens
|
||||||
request.addString("serviceName", config().serviceName);
|
request.addString("client", config().clientName);
|
||||||
request.addString("serviceToken", config().serviceToken);
|
|
||||||
|
|
||||||
//Add user tokens (if required)
|
//Add user tokens (if required)
|
||||||
if (request.needLogin) {
|
if (request.needLogin) {
|
||||||
final tokens = (await PreferencesHelper.getInstance()).getLoginTokens();
|
final token = (await PreferencesHelper.getInstance()).getLoginToken();
|
||||||
assert(tokens != null);
|
assert(token != null);
|
||||||
request.addString("userToken1", tokens.tokenOne);
|
request.addString("token", token);
|
||||||
request.addString("userToken2", tokens.tokenTwo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine server URL
|
// Determine server URL
|
||||||
@ -37,17 +36,38 @@ class APIHelper {
|
|||||||
else
|
else
|
||||||
url = Uri.https(config().apiServerName, path);
|
url = Uri.https(config().apiServerName, path);
|
||||||
|
|
||||||
final data = FormData.from(request.args);
|
final data = FormData.fromMap(request.args);
|
||||||
|
|
||||||
// Process files (if required)
|
// Process files (if required)
|
||||||
if (multipart)
|
if (multipart) {
|
||||||
request.files.forEach(
|
// Process filesystem files
|
||||||
(k, v) => data.add(k, UploadFileInfo(v, v.path.split("/").last)));
|
for (final key in request.files.keys) {
|
||||||
|
var v = request.files[key];
|
||||||
|
data.files.add(MapEntry(
|
||||||
|
key,
|
||||||
|
await MultipartFile.fromFile(v.path,
|
||||||
|
filename: v.path.split("/").last)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process in-memory files
|
||||||
|
for (final key in request.bytesFiles.keys) {
|
||||||
|
var v = request.bytesFiles[key];
|
||||||
|
data.files.add(MapEntry(
|
||||||
|
key,
|
||||||
|
MultipartFile.fromBytes(
|
||||||
|
v.bytes,
|
||||||
|
filename: v.filename.split("/").last,
|
||||||
|
contentType: v.type,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the request
|
// Execute the request
|
||||||
final response = await Dio().post(
|
final response = await Dio().post(
|
||||||
url.toString(),
|
url.toString(),
|
||||||
data: data,
|
data: data,
|
||||||
|
cancelToken: request.cancelToken,
|
||||||
|
onSendProgress: request.progressCallback,
|
||||||
options: Options(
|
options: Options(
|
||||||
receiveDataWhenStatusError: true,
|
receiveDataWhenStatusError: true,
|
||||||
validateStatus: (s) => true,
|
validateStatus: (s) => true,
|
||||||
@ -55,13 +75,18 @@ class APIHelper {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if login token is rejected by server
|
||||||
|
if (response.statusCode == 412)
|
||||||
|
EventsHelper.emit(InvalidLoginTokensEvent());
|
||||||
|
|
||||||
if (response.statusCode != HttpStatus.ok)
|
if (response.statusCode != HttpStatus.ok)
|
||||||
return APIResponse(response.statusCode, null);
|
return APIResponse(response.statusCode, response.data);
|
||||||
|
|
||||||
return APIResponse(response.statusCode, response.data);
|
return APIResponse(response.statusCode, response.data);
|
||||||
} catch (e) {
|
} catch (e, stack) {
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
print("Could not execute a request!");
|
print("Could not execute a request!");
|
||||||
|
print(stack);
|
||||||
return APIResponse(-1, null);
|
return APIResponse(-1, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
79
lib/helpers/calls_helper.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:comunic/helpers/websocket_helper.dart';
|
||||||
|
import 'package:comunic/lists/call_members_list.dart';
|
||||||
|
import 'package:comunic/models/call_config.dart';
|
||||||
|
import 'package:comunic/models/call_member.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
|
||||||
|
/// Calls helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class CallsHelper {
|
||||||
|
/// Join a call
|
||||||
|
static Future<void> join(int convID) async =>
|
||||||
|
await ws("calls/join", {"convID": convID});
|
||||||
|
|
||||||
|
/// Leave a call
|
||||||
|
static Future<void> leave(int convID) async =>
|
||||||
|
await ws("calls/leave", {"convID": convID});
|
||||||
|
|
||||||
|
/// Get calls configuration
|
||||||
|
static Future<CallConfig> getConfig() async {
|
||||||
|
final response = await ws("calls/config", {});
|
||||||
|
return CallConfig(
|
||||||
|
iceServers: response["iceServers"].cast<String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current call members
|
||||||
|
static Future<CallMembersList> getMembers(int callID) async =>
|
||||||
|
CallMembersList()
|
||||||
|
..addAll((await ws("calls/members", {"callID": callID}))
|
||||||
|
.map((f) => CallMember(
|
||||||
|
userID: f["userID"],
|
||||||
|
status: f["ready"] ? MemberStatus.READY : MemberStatus.JOINED,
|
||||||
|
))
|
||||||
|
.toList()
|
||||||
|
.cast<CallMember>());
|
||||||
|
|
||||||
|
/// Request an offer to access another peer's stream
|
||||||
|
static Future<void> requestOffer(int callID, int peerID) async =>
|
||||||
|
await ws("calls/request_offer", {"callID": callID, "peerID": peerID});
|
||||||
|
|
||||||
|
/// Send a Session Description message to the server
|
||||||
|
static Future<void> sendSessionDescription(
|
||||||
|
int callID, int peerID, RTCSessionDescription sdp) async =>
|
||||||
|
await ws("calls/signal", {
|
||||||
|
"callID": callID,
|
||||||
|
"peerID": peerID,
|
||||||
|
"type": "SDP",
|
||||||
|
"data": jsonEncode(sdp.toMap())
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Send an IceCandidate
|
||||||
|
static Future<void> sendIceCandidate(
|
||||||
|
int callID, int peerID, RTCIceCandidate candidate) async =>
|
||||||
|
await ws("calls/signal", {
|
||||||
|
"callID": callID,
|
||||||
|
"peerID": peerID,
|
||||||
|
"type": "CANDIDATE",
|
||||||
|
"data": jsonEncode(candidate.toMap())
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Mark ourselves as ready to stream to other peers
|
||||||
|
static Future<void> markPeerReady(int callID) async =>
|
||||||
|
await ws("calls/mark_ready", {"callID": callID});
|
||||||
|
|
||||||
|
/// Notify other peers that we stopped streaming
|
||||||
|
///
|
||||||
|
/// This method never throw
|
||||||
|
static Future<void> notifyStoppedStreaming(int callID) async {
|
||||||
|
try {
|
||||||
|
await ws("calls/stop_streaming", {"callID": callID});
|
||||||
|
} catch (e, stack) {
|
||||||
|
print("$e\n$stack");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
import 'package:comunic/models/comment.dart';
|
import 'package:comunic/models/comment.dart';
|
||||||
|
import 'package:comunic/models/displayed_content.dart';
|
||||||
import 'package:comunic/models/new_comment.dart';
|
import 'package:comunic/models/new_comment.dart';
|
||||||
|
|
||||||
/// Comments helper
|
/// Comments helper
|
||||||
@ -16,7 +17,7 @@ class CommentsHelper {
|
|||||||
"content": comment.hasContent ? comment.content : "",
|
"content": comment.hasContent ? comment.content : "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (comment.hasImage) request.addFile("image", comment.image);
|
if (comment.hasImage) request.addBytesFile("image", comment.image);
|
||||||
|
|
||||||
final response = await request.execWithFiles();
|
final response = await request.execWithFiles();
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ class CommentsHelper {
|
|||||||
userID: entry["userID"],
|
userID: entry["userID"],
|
||||||
postID: entry["postID"],
|
postID: entry["postID"],
|
||||||
timeSent: entry["time_sent"],
|
timeSent: entry["time_sent"],
|
||||||
content: entry["content"],
|
content: DisplayedString(entry["content"]),
|
||||||
imageURL: entry["img_url"],
|
imageURL: entry["img_url"],
|
||||||
likes: entry["likes"],
|
likes: entry["likes"],
|
||||||
userLike: entry["userlike"],
|
userLike: entry["userlike"],
|
||||||
|
@ -1,15 +1,25 @@
|
|||||||
import 'package:comunic/helpers/database/conversation_messages_database_helper.dart';
|
import 'package:comunic/helpers/serialization/conversation_message_serialization_helper.dart';
|
||||||
import 'package:comunic/helpers/database/conversations_database_helper.dart';
|
import 'package:comunic/helpers/serialization/conversations_serialization_helper.dart';
|
||||||
import 'package:comunic/helpers/users_helper.dart';
|
import 'package:comunic/helpers/users_helper.dart';
|
||||||
|
import 'package:comunic/helpers/websocket_helper.dart';
|
||||||
import 'package:comunic/lists/conversation_messages_list.dart';
|
import 'package:comunic/lists/conversation_messages_list.dart';
|
||||||
import 'package:comunic/lists/conversations_list.dart';
|
import 'package:comunic/lists/conversations_list.dart';
|
||||||
|
import 'package:comunic/lists/unread_conversations_list.dart';
|
||||||
import 'package:comunic/lists/users_list.dart';
|
import 'package:comunic/lists/users_list.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
import 'package:comunic/models/api_response.dart';
|
import 'package:comunic/models/api_response.dart';
|
||||||
import 'package:comunic/models/conversation.dart';
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
import 'package:comunic/models/conversation_member.dart';
|
||||||
import 'package:comunic/models/conversation_message.dart';
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
import 'package:comunic/models/displayed_content.dart';
|
||||||
|
import 'package:comunic/models/new_conversation.dart';
|
||||||
import 'package:comunic/models/new_conversation_message.dart';
|
import 'package:comunic/models/new_conversation_message.dart';
|
||||||
|
import 'package:comunic/models/new_conversation_settings.dart';
|
||||||
|
import 'package:comunic/models/unread_conversation.dart';
|
||||||
import 'package:comunic/utils/account_utils.dart';
|
import 'package:comunic/utils/account_utils.dart';
|
||||||
|
import 'package:comunic/utils/color_utils.dart';
|
||||||
|
import 'package:comunic/utils/dart_color.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
/// Conversation helper
|
/// Conversation helper
|
||||||
@ -19,124 +29,149 @@ import 'package:meta/meta.dart';
|
|||||||
enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED }
|
enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED }
|
||||||
|
|
||||||
class ConversationsHelper {
|
class ConversationsHelper {
|
||||||
final ConversationsDatabaseHelper _conversationsDatabaseHelper =
|
static final _registeredConversations = Map<int, int>();
|
||||||
ConversationsDatabaseHelper();
|
|
||||||
final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper =
|
|
||||||
ConversationMessagesDatabaseHelper();
|
|
||||||
|
|
||||||
/// Create a new conversation
|
/// Create a new conversation
|
||||||
///
|
///
|
||||||
/// Return the ID of the newly created conversation or -1 in case of failure
|
/// Return the ID of the newly created conversation
|
||||||
Future<int> createConversation(Conversation settings) async {
|
///
|
||||||
final response =
|
/// Throws in case of failure
|
||||||
await APIRequest(uri: "conversations/create", needLogin: true, args: {
|
static Future<int> createConversation(NewConversation settings) async {
|
||||||
"name": settings.hasName ? settings.name : "false",
|
final response = await APIRequest.withLogin("conversations/create", args: {
|
||||||
"follow": settings.following ? "true" : "false",
|
"name": settings.name ?? "",
|
||||||
"users": settings.members.join(",")
|
"follow": settings.follow ? "true" : "false",
|
||||||
}).exec();
|
"users": settings.members.join(","),
|
||||||
|
"color": colorToHex(settings.color)
|
||||||
if (response.code != 200) return -1;
|
})
|
||||||
|
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
return response.getObject()["conversationID"];
|
return response.getObject()["conversationID"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a member to a conversation.
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> addMember(int convID, int userID) async =>
|
||||||
|
await APIRequest.withLogin("conversations/addMember")
|
||||||
|
.addInt("convID", convID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Remove a member from a conversation.
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> removeMember(int convID, int userID) async =>
|
||||||
|
await APIRequest.withLogin("conversations/removeMember")
|
||||||
|
.addInt("convID", convID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Update admin status of a user in a conversation
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> setAdmin(int convID, int userID, bool admin) async =>
|
||||||
|
await APIRequest.withLogin("conversations/setAdmin")
|
||||||
|
.addInt("convID", convID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.addBool("setAdmin", admin)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
/// Update an existing conversation
|
/// Update an existing conversation
|
||||||
///
|
///
|
||||||
/// Returns a boolean depending of the success of the operation
|
/// Throws in case of failure
|
||||||
Future<bool> updateConversation(Conversation settings) async {
|
static Future<void> updateConversation(
|
||||||
final request =
|
NewConversationsSettings settings) async {
|
||||||
APIRequest(uri: "conversations/updateSettings", needLogin: true, args: {
|
final request = APIRequest.withLogin("conversations/updateSettings")
|
||||||
"conversationID": settings.id.toString(),
|
.addInt("conversationID", settings.convID)
|
||||||
"following": settings.following ? "true" : "false"
|
.addBool("following", settings.following);
|
||||||
});
|
|
||||||
|
|
||||||
// Update all conversation settings, if possible
|
// Update conversation settings
|
||||||
if (settings.isOwner) {
|
if (settings.isComplete)
|
||||||
request.addString("name", settings.hasName ? settings.name : "false");
|
request
|
||||||
request.addString("members", settings.members.join(","));
|
.addString("name", settings.name ?? "")
|
||||||
}
|
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
|
||||||
|
.addString("color", colorToHex(settings.color));
|
||||||
|
|
||||||
final response = await request.exec();
|
await request.execWithThrow();
|
||||||
|
|
||||||
if (response.code != 200) return false;
|
// Delete old conversation entry from the database
|
||||||
|
await ConversationsSerializationHelper()
|
||||||
//Delete old conversation entry from the database
|
.removeElement((t) => t.id == settings.convID);
|
||||||
await _conversationsDatabaseHelper.delete(settings.id);
|
|
||||||
|
|
||||||
// Success
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a new conversation logo
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> changeImage(int convID, BytesFile file) async =>
|
||||||
|
await APIRequest.withLogin("conversations/change_image")
|
||||||
|
.addInt("convID", convID)
|
||||||
|
.addBytesFile("file", file)
|
||||||
|
.execWithFilesAndThrow();
|
||||||
|
|
||||||
|
/// Remove conversation logo
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> removeLogo(int convID) async =>
|
||||||
|
await APIRequest.withLogin("conversations/delete_image")
|
||||||
|
.addInt("convID", convID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
/// Delete a conversation specified by its [id]
|
/// Delete a conversation specified by its [id]
|
||||||
Future<bool> deleteConversation(int id) async {
|
Future<void> deleteConversation(int id) async =>
|
||||||
final response = await APIRequest(
|
await APIRequest.withLogin("conversations/delete")
|
||||||
uri: "conversations/delete",
|
.addInt("conversationID", id)
|
||||||
needLogin: true,
|
.execWithThrow();
|
||||||
args: {
|
|
||||||
"conversationID": id.toString(),
|
|
||||||
},
|
|
||||||
).exec();
|
|
||||||
|
|
||||||
return response.code == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Download the list of conversations from the server
|
/// Download the list of conversations from the server
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
Future<ConversationsList> downloadList() async {
|
Future<ConversationsList> downloadList() async {
|
||||||
final response =
|
final response =
|
||||||
await APIRequest(uri: "conversations/getList", needLogin: true).exec();
|
await APIRequest.withLogin("conversations/getList").execWithThrow();
|
||||||
|
|
||||||
if (response.code != 200) return null;
|
ConversationsList list = ConversationsList();
|
||||||
|
response.getArray().forEach((f) => list.add(apiToConversation(f)));
|
||||||
|
|
||||||
try {
|
// Update the database
|
||||||
ConversationsList list = ConversationsList();
|
await ConversationsSerializationHelper().setList(list);
|
||||||
response.getArray().forEach((f) => list.add(_apiToConversation(f)));
|
|
||||||
|
|
||||||
// Update the database
|
return list;
|
||||||
await _conversationsDatabaseHelper.clearTable();
|
|
||||||
await _conversationsDatabaseHelper.insertAll(list);
|
|
||||||
|
|
||||||
return list;
|
|
||||||
} on Exception catch (e) {
|
|
||||||
print(e.toString());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the local list of conversations
|
/// Get the local list of conversations
|
||||||
Future<ConversationsList> getCachedList() async {
|
Future<ConversationsList> getCachedList() async {
|
||||||
final list = await _conversationsDatabaseHelper.getAll();
|
final list = await ConversationsSerializationHelper().getList();
|
||||||
list.sort();
|
list.sort();
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get information about a single conversation specified by its [id]
|
/// Get information about a single conversation specified by its [id]
|
||||||
Future<Conversation> _downloadSingle(int id) async {
|
Future<Conversation> _downloadSingle(int id) async {
|
||||||
try {
|
final response = await APIRequest(
|
||||||
final response = await APIRequest(
|
uri: "conversations/get_single",
|
||||||
uri: "conversations/getInfoOne",
|
needLogin: true,
|
||||||
needLogin: true,
|
args: {"conversationID": id.toString()}).execWithThrow();
|
||||||
args: {"conversationID": id.toString()}).exec();
|
|
||||||
|
|
||||||
if (response.code != 200) return null;
|
final conversation = apiToConversation(response.getObject());
|
||||||
|
|
||||||
final conversation = _apiToConversation(response.getObject());
|
await ConversationsSerializationHelper()
|
||||||
_conversationsDatabaseHelper.insertOrUpdate(conversation);
|
.insertOrReplaceElement((c) => c.id == conversation.id, conversation);
|
||||||
return conversation;
|
return conversation;
|
||||||
} on Exception catch (e) {
|
|
||||||
print(e.toString());
|
|
||||||
print("Could not get information about a single conversation !");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get information about a single conversation. If [force] is set to false,
|
/// Get information about a conversation. If [force] is set to false, a
|
||||||
/// cached version of the conversation will be used, else it will always get
|
/// cached version of the conversation will be used, else it will always get
|
||||||
/// the information from the server
|
/// the information from the server. The method throws an [Exception] in
|
||||||
|
/// case of failure
|
||||||
|
///
|
||||||
|
/// Return value of this method is never null.
|
||||||
Future<Conversation> getSingle(int id, {bool force = false}) async {
|
Future<Conversation> getSingle(int id, {bool force = false}) async {
|
||||||
if (force || !await _conversationsDatabaseHelper.has(id))
|
if (force ||
|
||||||
|
!await ConversationsSerializationHelper().any((c) => c.id == id))
|
||||||
return await _downloadSingle(id);
|
return await _downloadSingle(id);
|
||||||
else
|
else
|
||||||
return _conversationsDatabaseHelper.get(id);
|
return await ConversationsSerializationHelper().get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the name of a [conversation]. This requires information
|
/// Get the name of a [conversation]. This requires information
|
||||||
@ -148,9 +183,9 @@ class ConversationsHelper {
|
|||||||
String name = "";
|
String name = "";
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < 3 && i < conversation.members.length; i++)
|
for (int i = 0; i < 3 && i < conversation.members.length; i++)
|
||||||
if (conversation.members[i] != userID()) {
|
if (conversation.members[i].userID != userID()) {
|
||||||
name += (count > 0 ? ", " : "") +
|
name += (count > 0 ? ", " : "") +
|
||||||
users.getUser(conversation.members[i]).fullName;
|
users.getUser(conversation.members[i].userID).fullName;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,6 +197,8 @@ class ConversationsHelper {
|
|||||||
/// Search and return a private conversation with a given [userID]. If such
|
/// Search and return a private conversation with a given [userID]. If such
|
||||||
/// conversation does not exists, it is created if [allowCreate] is set to
|
/// conversation does not exists, it is created if [allowCreate] is set to
|
||||||
/// true
|
/// true
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
Future<int> getPrivate(int userID, {bool allowCreate = true}) async {
|
Future<int> getPrivate(int userID, {bool allowCreate = true}) async {
|
||||||
final response = await APIRequest(
|
final response = await APIRequest(
|
||||||
uri: "conversations/getPrivate",
|
uri: "conversations/getPrivate",
|
||||||
@ -170,68 +207,78 @@ class ConversationsHelper {
|
|||||||
"otherUser": userID.toString(),
|
"otherUser": userID.toString(),
|
||||||
"allowCreate": allowCreate.toString()
|
"allowCreate": allowCreate.toString()
|
||||||
},
|
},
|
||||||
).exec();
|
).execWithThrow();
|
||||||
|
|
||||||
if (response.code != 200) return null;
|
|
||||||
|
|
||||||
// Get and return conversation ID
|
// Get and return conversation ID
|
||||||
try {
|
return int.parse(response.getObject()["conversationsID"][0].toString());
|
||||||
return int.parse(response.getObject()["conversationsID"][0].toString());
|
|
||||||
} catch (e) {
|
|
||||||
e.toString();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asynchronously get the name fo the conversation
|
/// Asynchronously get the name of the conversation
|
||||||
///
|
///
|
||||||
/// Unlike the synchronous method, this method does not need information
|
/// Unlike the synchronous method, this method does not need information
|
||||||
/// about the members of the conversation
|
/// about the members of the conversation
|
||||||
///
|
///
|
||||||
/// Returns null in case of failure
|
/// Throws an exception in case of failure
|
||||||
static Future<String> getConversationNameAsync(
|
static Future<String> getConversationNameAsync(
|
||||||
Conversation conversation) async {
|
Conversation conversation) async {
|
||||||
if (conversation.hasName) return conversation.name;
|
if (conversation.hasName) return conversation.name;
|
||||||
|
|
||||||
//Get information about the members of the conversation
|
//Get information about the members of the conversation
|
||||||
final members = await UsersHelper().getUsersInfo(conversation.members);
|
final members = await UsersHelper().getList(conversation.membersID);
|
||||||
|
|
||||||
if (members == null) return null;
|
|
||||||
|
|
||||||
return ConversationsHelper.getConversationName(conversation, members);
|
return ConversationsHelper.getConversationName(conversation, members);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turn an API entry into a [Conversation] object
|
/// Turn an API entry into a [Conversation] object
|
||||||
Conversation _apiToConversation(Map<String, dynamic> map) {
|
static Conversation apiToConversation(Map<String, dynamic> map) {
|
||||||
return Conversation(
|
return Conversation(
|
||||||
id: map["ID"],
|
id: map["id"],
|
||||||
ownerID: map["ID_owner"],
|
lastActivity: map["last_activity"],
|
||||||
lastActive: map["last_active"],
|
name: map["name"],
|
||||||
name: map["name"] == false ? null : map["name"],
|
color: map["color"] == null ? null : HexColor(map["color"]),
|
||||||
following: map["following"] == 1,
|
logoURL: map["logo"],
|
||||||
sawLastMessage: map["saw_last_message"] == 1,
|
groupID: map["group_id"],
|
||||||
members: map["members"].map<int>((f) => int.parse(f)).toList(),
|
members: map["members"]
|
||||||
);
|
.cast<Map<String, dynamic>>()
|
||||||
|
.map(apiToConversationMember)
|
||||||
|
.toList()
|
||||||
|
.cast<ConversationMember>(),
|
||||||
|
canEveryoneAddMembers: map["can_everyone_add_members"],
|
||||||
|
callCapabilities: map["can_have_video_call"]
|
||||||
|
? CallCapabilities.VIDEO
|
||||||
|
: (map["can_have_call"]
|
||||||
|
? CallCapabilities.AUDIO
|
||||||
|
: CallCapabilities.NONE),
|
||||||
|
isHavingCall: map["has_call_now"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ConversationMember apiToConversationMember(Map<String, dynamic> map) =>
|
||||||
|
ConversationMember(
|
||||||
|
userID: map["user_id"],
|
||||||
|
lastMessageSeen: map["last_message_seen"],
|
||||||
|
lastAccessTime: map["last_access"],
|
||||||
|
following: map["following"],
|
||||||
|
isAdmin: map["is_admin"],
|
||||||
|
);
|
||||||
|
|
||||||
/// Parse a list of messages given by the server
|
/// Parse a list of messages given by the server
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
Future<ConversationMessagesList> _parseConversationMessageFromServer(
|
Future<ConversationMessagesList> _parseConversationMessageFromServer(
|
||||||
int conversationID, APIResponse response) async {
|
int conversationID, APIResponse response) async {
|
||||||
if (response.code != 200) return null;
|
response.assertOk();
|
||||||
|
|
||||||
// Parse the response of the server
|
// Parse the response of the server
|
||||||
ConversationMessagesList list = ConversationMessagesList();
|
ConversationMessagesList list = ConversationMessagesList();
|
||||||
response.getArray().forEach((f) {
|
response.getArray().forEach((f) {
|
||||||
list.add(
|
list.add(
|
||||||
_apiToConversationMessage(
|
apiToConversationMessage(f),
|
||||||
conversationID: conversationID,
|
|
||||||
map: f,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save messages in the cache
|
// Save messages in the cache
|
||||||
_conversationMessagesDatabaseHelper.insertOrUpdateAll(list);
|
await ConversationsMessagesSerializationHelper(conversationID)
|
||||||
|
.insertOrReplaceAll(list);
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@ -240,6 +287,8 @@ class ConversationsHelper {
|
|||||||
///
|
///
|
||||||
/// Set [lastMessageID] to 0 to specify that we do not have any message of the
|
/// Set [lastMessageID] to 0 to specify that we do not have any message of the
|
||||||
/// conversation yet or another value else
|
/// conversation yet or another value else
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
Future<ConversationMessagesList> _downloadNewMessagesSingle(
|
Future<ConversationMessagesList> _downloadNewMessagesSingle(
|
||||||
int conversationID,
|
int conversationID,
|
||||||
{int lastMessageID = 0}) async {
|
{int lastMessageID = 0}) async {
|
||||||
@ -250,26 +299,26 @@ class ConversationsHelper {
|
|||||||
args: {
|
args: {
|
||||||
"conversationID": conversationID.toString(),
|
"conversationID": conversationID.toString(),
|
||||||
"last_message_id": lastMessageID.toString()
|
"last_message_id": lastMessageID.toString()
|
||||||
}).exec();
|
}).execWithThrow();
|
||||||
|
|
||||||
return await _parseConversationMessageFromServer(conversationID, response);
|
return await _parseConversationMessageFromServer(conversationID, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get older messages for a given conversation from an online source
|
/// Get older messages for a given conversation from an online source
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
Future<ConversationMessagesList> getOlderMessages({
|
Future<ConversationMessagesList> getOlderMessages({
|
||||||
@required int conversationID,
|
@required int conversationID,
|
||||||
@required int oldestMessagesID,
|
@required int oldestMessagesID,
|
||||||
int limit = 15,
|
int limit = 15,
|
||||||
}) async {
|
}) async {
|
||||||
// Perform the request online
|
// Perform the request online
|
||||||
final response = await APIRequest(
|
final response =
|
||||||
uri: "conversations/get_older_messages",
|
await APIRequest.withLogin("conversations/get_older_messages", args: {
|
||||||
needLogin: true,
|
"conversationID": conversationID.toString(),
|
||||||
args: {
|
"oldest_message_id": oldestMessagesID.toString(),
|
||||||
"conversationID": conversationID.toString(),
|
"limit": limit.toString()
|
||||||
"oldest_message_id": oldestMessagesID.toString(),
|
}).execWithThrow();
|
||||||
"limit": limit.toString()
|
|
||||||
}).exec();
|
|
||||||
|
|
||||||
return await _parseConversationMessageFromServer(conversationID, response);
|
return await _parseConversationMessageFromServer(conversationID, response);
|
||||||
}
|
}
|
||||||
@ -279,6 +328,8 @@ class ConversationsHelper {
|
|||||||
/// If [lastMessageID] is set to 0 then we retrieve the last messages of
|
/// If [lastMessageID] is set to 0 then we retrieve the last messages of
|
||||||
/// the conversation.
|
/// the conversation.
|
||||||
/// Otherwise [lastMessageID] contains the ID of the last known message
|
/// Otherwise [lastMessageID] contains the ID of the last known message
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
Future<ConversationMessagesList> getNewMessages(
|
Future<ConversationMessagesList> getNewMessages(
|
||||||
{@required int conversationID,
|
{@required int conversationID,
|
||||||
int lastMessageID = 0,
|
int lastMessageID = 0,
|
||||||
@ -287,35 +338,32 @@ class ConversationsHelper {
|
|||||||
return await _downloadNewMessagesSingle(conversationID,
|
return await _downloadNewMessagesSingle(conversationID,
|
||||||
lastMessageID: lastMessageID);
|
lastMessageID: lastMessageID);
|
||||||
else
|
else
|
||||||
return await _conversationMessagesDatabaseHelper
|
return await ConversationsMessagesSerializationHelper(conversationID)
|
||||||
.getAllMessagesConversations(conversationID,
|
.getList();
|
||||||
lastMessageID: lastMessageID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a single conversation message from the local database
|
|
||||||
///
|
|
||||||
/// Returns the message if found or null in case of failure
|
|
||||||
Future<ConversationMessage> getSingleMessageFromCache(int messageID) async {
|
|
||||||
return await _conversationMessagesDatabaseHelper.get(messageID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a new message to the server
|
/// Send a new message to the server
|
||||||
Future<SendMessageResult> sendMessage(NewConversationMessage message) async {
|
Future<SendMessageResult> sendMessage(
|
||||||
final request = APIRequest(
|
NewConversationMessage message, {
|
||||||
uri: "conversations/sendMessage",
|
ProgressCallback sendProgress,
|
||||||
needLogin: true,
|
CancelToken cancelToken,
|
||||||
args: {
|
}) async {
|
||||||
"conversationID": message.conversationID.toString(),
|
final request = APIRequest.withLogin("conversations/sendMessage")
|
||||||
"message": message.hasMessage ? message.message : ""
|
.addInt("conversationID", message.conversationID)
|
||||||
},
|
.addString("message", message.hasMessage ? message.message : "");
|
||||||
);
|
|
||||||
|
|
||||||
//Check for image
|
request.progressCallback = sendProgress;
|
||||||
if (message.hasImage) request.addFile("image", message.image);
|
request.cancelToken = cancelToken;
|
||||||
|
|
||||||
|
// Check for file
|
||||||
|
if (message.hasFile) request.addBytesFile("file", message.file);
|
||||||
|
|
||||||
|
if (message.hasThumbnail)
|
||||||
|
request.addBytesFile("thumbnail", message.thumbnail);
|
||||||
|
|
||||||
//Send the message
|
//Send the message
|
||||||
APIResponse response;
|
APIResponse response;
|
||||||
if (!message.hasImage)
|
if (!message.hasFile)
|
||||||
response = await request.exec();
|
response = await request.exec();
|
||||||
else
|
else
|
||||||
response = await request.execWithFiles();
|
response = await request.execWithFiles();
|
||||||
@ -327,6 +375,15 @@ class ConversationsHelper {
|
|||||||
return SendMessageResult.SUCCESS;
|
return SendMessageResult.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Save / Update a message into the database
|
||||||
|
Future<void> saveMessage(ConversationMessage msg) async =>
|
||||||
|
await ConversationsMessagesSerializationHelper(msg.convID)
|
||||||
|
.insertOrReplace(msg);
|
||||||
|
|
||||||
|
/// Remove a message from the database
|
||||||
|
Future<void> removeMessage(ConversationMessage msg) async =>
|
||||||
|
await ConversationsMessagesSerializationHelper(msg.convID).remove(msg);
|
||||||
|
|
||||||
/// Update a message content
|
/// Update a message content
|
||||||
Future<bool> updateMessage(int id, String newContent) async {
|
Future<bool> updateMessage(int id, String newContent) async {
|
||||||
final response = await APIRequest(
|
final response = await APIRequest(
|
||||||
@ -336,9 +393,7 @@ class ConversationsHelper {
|
|||||||
|
|
||||||
if (response.code != 200) return false;
|
if (response.code != 200) return false;
|
||||||
|
|
||||||
// Update the message content locally
|
return true;
|
||||||
return await _conversationMessagesDatabaseHelper.updateMessageContent(
|
|
||||||
id: id, newContent: newContent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete permanently a message specified by its [id]
|
/// Delete permanently a message specified by its [id]
|
||||||
@ -351,22 +406,111 @@ class ConversationsHelper {
|
|||||||
|
|
||||||
if (response.code != 200) return false;
|
if (response.code != 200) return false;
|
||||||
|
|
||||||
// Delete the message locally
|
return true;
|
||||||
return await _conversationMessagesDatabaseHelper.delete(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the list of unread conversations
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<UnreadConversationsList> getListUnread() async {
|
||||||
|
final list = (await APIRequest.withLogin("conversations/get_list_unread")
|
||||||
|
.execWithThrow())
|
||||||
|
.getArray();
|
||||||
|
|
||||||
|
return UnreadConversationsList()
|
||||||
|
..addAll(list.map((f) => UnreadConversation(
|
||||||
|
conv: apiToConversation(f["conv"]),
|
||||||
|
message: apiToConversationMessage(f["message"]),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a conversation : ask the server to notify about updates to the
|
||||||
|
/// conversation through WebSocket
|
||||||
|
Future<void> registerConversationEvents(int id) async {
|
||||||
|
if (_registeredConversations.containsKey(id))
|
||||||
|
_registeredConversations[id]++;
|
||||||
|
else {
|
||||||
|
_registeredConversations[id] = 1;
|
||||||
|
await ws("\$main/register_conv", {"convID": id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un-register to conversation update events
|
||||||
|
Future<void> unregisterConversationEvents(int id) async {
|
||||||
|
if (!_registeredConversations.containsKey(id)) return;
|
||||||
|
|
||||||
|
_registeredConversations[id]--;
|
||||||
|
|
||||||
|
if (_registeredConversations[id] <= 0) {
|
||||||
|
_registeredConversations.remove(id);
|
||||||
|
await ws("\$main/unregister_conv", {"convID": id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a notification to inform that the user is writing a message
|
||||||
|
static Future<void> sendWritingEvent(int convID) async =>
|
||||||
|
await ws("conversations/is_writing", {"convID": convID});
|
||||||
|
|
||||||
/// Turn an API response into a ConversationMessage object
|
/// Turn an API response into a ConversationMessage object
|
||||||
ConversationMessage _apiToConversationMessage({
|
static ConversationMessage apiToConversationMessage(
|
||||||
@required int conversationID,
|
Map<String, dynamic> map,
|
||||||
@required Map<String, dynamic> map,
|
) {
|
||||||
}) {
|
var file;
|
||||||
|
if (map["file"] != null) {
|
||||||
|
final fileMap = map["file"];
|
||||||
|
file = ConversationMessageFile(
|
||||||
|
url: fileMap["url"],
|
||||||
|
size: fileMap["size"],
|
||||||
|
name: fileMap["name"],
|
||||||
|
thumbnail: fileMap["thumbnail"],
|
||||||
|
type: fileMap["type"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverMessage;
|
||||||
|
if (map["server_message"] != null) {
|
||||||
|
final srvMessageMap = map["server_message"];
|
||||||
|
|
||||||
|
var messageType;
|
||||||
|
switch (srvMessageMap["type"]) {
|
||||||
|
case "user_created_conv":
|
||||||
|
messageType = ConversationServerMessageType.USER_CREATED_CONVERSATION;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "user_added_another":
|
||||||
|
messageType = ConversationServerMessageType.USER_ADDED_ANOTHER_USER;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "user_left":
|
||||||
|
messageType = ConversationServerMessageType.USER_LEFT_CONV;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "user_removed_another":
|
||||||
|
messageType = ConversationServerMessageType.USER_REMOVED_ANOTHER_USER;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Exception(
|
||||||
|
"${srvMessageMap["type"]} is an unknown server message type!");
|
||||||
|
}
|
||||||
|
|
||||||
|
serverMessage = ConversationServerMessage(
|
||||||
|
type: messageType,
|
||||||
|
userID: srvMessageMap["user_id"],
|
||||||
|
userWhoAdded: srvMessageMap["user_who_added"],
|
||||||
|
userAdded: srvMessageMap["user_added"],
|
||||||
|
userWhoRemoved: srvMessageMap["user_who_removed"],
|
||||||
|
userRemoved: srvMessageMap["user_removed"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ConversationMessage(
|
return ConversationMessage(
|
||||||
id: map["ID"],
|
id: map["id"],
|
||||||
conversationID: conversationID,
|
convID: map["conv_id"],
|
||||||
userID: map["ID_user"],
|
userID: map["user_id"],
|
||||||
timeInsert: map["time_insert"],
|
timeSent: map["time_sent"],
|
||||||
message: map["message"],
|
message: DisplayedString(map["message"] ?? ""),
|
||||||
imageURL: map["image_path"],
|
file: file,
|
||||||
);
|
serverMessage: serverMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
import 'package:comunic/helpers/database/database_contract.dart';
|
|
||||||
import 'package:comunic/helpers/database/model_database_helper.dart';
|
|
||||||
import 'package:comunic/lists/conversation_messages_list.dart';
|
|
||||||
import 'package:comunic/models/conversation_message.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
/// Conversation messages database helper
|
|
||||||
///
|
|
||||||
/// @author Pierre HUBERT
|
|
||||||
|
|
||||||
class ConversationMessagesDatabaseHelper
|
|
||||||
extends ModelDatabaseHelper<ConversationMessage> {
|
|
||||||
@override
|
|
||||||
ConversationMessage initializeFromMap(Map<String, dynamic> map) {
|
|
||||||
return ConversationMessage.fromMap(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String tableName() {
|
|
||||||
return ConversationsMessagesTableContract.TABLE_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all the message cached for a given conversation
|
|
||||||
Future<ConversationMessagesList> getAllMessagesConversations(
|
|
||||||
int conversationID,
|
|
||||||
{int lastMessageID = 0}) async {
|
|
||||||
final list = await getMultiple(
|
|
||||||
where: "${ConversationsMessagesTableContract.C_CONVERSATION_ID} = ? "
|
|
||||||
"AND ${BaseTableContract.C_ID} > ?",
|
|
||||||
whereArgs: [conversationID, lastMessageID],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Turn the list into a conversation messages list
|
|
||||||
ConversationMessagesList finalList = ConversationMessagesList();
|
|
||||||
finalList.addAll(list);
|
|
||||||
return finalList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the content of a message
|
|
||||||
Future<bool> updateMessageContent({
|
|
||||||
@required int id,
|
|
||||||
@required String newContent,
|
|
||||||
}) async {
|
|
||||||
assert(id != null);
|
|
||||||
assert(newContent != null);
|
|
||||||
|
|
||||||
final message = await get(id);
|
|
||||||
|
|
||||||
if(message == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Update the conversation message using the map
|
|
||||||
final map = message.toMap();
|
|
||||||
map[ConversationsMessagesTableContract.C_MESSAGE] = newContent;
|
|
||||||
|
|
||||||
await insertOrUpdate(ConversationMessage.fromMap(map));
|
|
||||||
|
|
||||||
return true; // Success
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import 'package:comunic/helpers/database/database_contract.dart';
|
|
||||||
import 'package:comunic/helpers/database/model_database_helper.dart';
|
|
||||||
import 'package:comunic/lists/conversations_list.dart';
|
|
||||||
import 'package:comunic/models/conversation.dart';
|
|
||||||
|
|
||||||
/// Conversations database helper
|
|
||||||
///
|
|
||||||
/// @author Pierre HUBERT
|
|
||||||
|
|
||||||
class ConversationsDatabaseHelper extends ModelDatabaseHelper<Conversation> {
|
|
||||||
@override
|
|
||||||
Conversation initializeFromMap(Map<String, dynamic> map) {
|
|
||||||
return Conversation.fromMap(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String tableName() {
|
|
||||||
return ConversationTableContract.TABLE_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ConversationsList> getAll() async {
|
|
||||||
ConversationsList list = ConversationsList();
|
|
||||||
list.addAll(await super.getAll());
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
/// Main information
|
/// Main information
|
||||||
class DatabaseContract {
|
class DatabaseContract {
|
||||||
static const DATABASE_VERSION = 1;
|
static const DATABASE_VERSION = 2;
|
||||||
static const DATABASE_FILE_NAME = "database.sqlite";
|
static const DATABASE_FILE_NAME = "database.sqlite";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,32 +19,10 @@ abstract class UserTableContract {
|
|||||||
static const C_ID = BaseTableContract.C_ID;
|
static const C_ID = BaseTableContract.C_ID;
|
||||||
static const C_FIRST_NAME = "first_name";
|
static const C_FIRST_NAME = "first_name";
|
||||||
static const C_LAST_NAME = "last_name";
|
static const C_LAST_NAME = "last_name";
|
||||||
static const C_VISIBILITY = "visibility";
|
static const C_VISIBILITY = "visibility";
|
||||||
static const C_VIRTUAL_DIRECTORY = "virtual_directory";
|
static const C_VIRTUAL_DIRECTORY = "virtual_directory";
|
||||||
static const C_ACCOUNT_IMAGE_URL = "account_image_url";
|
static const C_ACCOUNT_IMAGE_URL = "account_image_url";
|
||||||
}
|
static const C_CUSTOM_EMOJIES = "custom_emojies";
|
||||||
|
|
||||||
/// Conversations table contract
|
|
||||||
abstract class ConversationTableContract {
|
|
||||||
static const TABLE_NAME = "conversations";
|
|
||||||
static const C_ID = BaseTableContract.C_ID;
|
|
||||||
static const C_OWNER_ID = "owner_id";
|
|
||||||
static const C_LAST_ACTIVE = "last_active";
|
|
||||||
static const C_NAME = "name";
|
|
||||||
static const C_FOLLOWING = "following";
|
|
||||||
static const C_SAW_LAST_MESSAGE = "saw_last_message";
|
|
||||||
static const C_MEMBERS = "members";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Conversations messages table contract
|
|
||||||
abstract class ConversationsMessagesTableContract {
|
|
||||||
static const TABLE_NAME = "conversations_messages";
|
|
||||||
static const C_ID = BaseTableContract.C_ID;
|
|
||||||
static const C_CONVERSATION_ID = "conversation_id";
|
|
||||||
static const C_USER_ID = "user_id";
|
|
||||||
static const C_TIME_INSERT = "time_insert";
|
|
||||||
static const C_MESSAGE = "message";
|
|
||||||
static const C_IMAGE_URL = "image_url";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Friends table contract
|
/// Friends table contract
|
||||||
@ -55,4 +33,4 @@ abstract class FriendsListTableContract {
|
|||||||
static const C_LAST_ACTIVE = "last_active";
|
static const C_LAST_ACTIVE = "last_active";
|
||||||
static const C_FOLLOWING = "following";
|
static const C_FOLLOWING = "following";
|
||||||
static const C_CAN_POST_TEXTS = "can_post_texts";
|
static const C_CAN_POST_TEXTS = "can_post_texts";
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:comunic/helpers/database/database_contract.dart';
|
import 'package:comunic/helpers/database/database_contract.dart';
|
||||||
|
import 'package:connectivity/connectivity.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
@ -29,6 +30,13 @@ abstract class DatabaseHelper {
|
|||||||
return _db;
|
return _db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cleanup database
|
||||||
|
static Future<void> cleanUpDatabase() async {
|
||||||
|
// If connected to a network, cleanup user information
|
||||||
|
if (await Connectivity().checkConnectivity() != ConnectivityResult.none)
|
||||||
|
await _db.execute("DELETE FROM ${UserTableContract.TABLE_NAME}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform database update
|
/// Perform database update
|
||||||
///
|
///
|
||||||
/// Currently : delete all the database tables and initialize it again
|
/// Currently : delete all the database tables and initialize it again
|
||||||
@ -37,17 +45,9 @@ abstract class DatabaseHelper {
|
|||||||
// Drop users table
|
// Drop users table
|
||||||
await db.execute("DROP TABLE IF EXISTS ${UserTableContract.TABLE_NAME}");
|
await db.execute("DROP TABLE IF EXISTS ${UserTableContract.TABLE_NAME}");
|
||||||
|
|
||||||
// Drop conversations table
|
|
||||||
await db.execute(
|
|
||||||
"DROP TABLE IF EXISTS ${ConversationTableContract.TABLE_NAME}");
|
|
||||||
|
|
||||||
// Drop conversations messages table
|
|
||||||
await db.execute(
|
|
||||||
"DROP TABLE IF EXISTS ${ConversationsMessagesTableContract.TABLE_NAME}");
|
|
||||||
|
|
||||||
// Drop friends list table
|
// Drop friends list table
|
||||||
await db.execute(
|
await db
|
||||||
"DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}");
|
.execute("DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}");
|
||||||
|
|
||||||
// Initialize database again
|
// Initialize database again
|
||||||
await _initializeDatabase(db, newVersion);
|
await _initializeDatabase(db, newVersion);
|
||||||
@ -62,29 +62,8 @@ abstract class DatabaseHelper {
|
|||||||
"${UserTableContract.C_LAST_NAME} TEXT, "
|
"${UserTableContract.C_LAST_NAME} TEXT, "
|
||||||
"${UserTableContract.C_VISIBILITY} TEXT, "
|
"${UserTableContract.C_VISIBILITY} TEXT, "
|
||||||
"${UserTableContract.C_VIRTUAL_DIRECTORY} TEXT, "
|
"${UserTableContract.C_VIRTUAL_DIRECTORY} TEXT, "
|
||||||
"${UserTableContract.C_ACCOUNT_IMAGE_URL} TEXT"
|
"${UserTableContract.C_ACCOUNT_IMAGE_URL} TEXT, "
|
||||||
")");
|
"${UserTableContract.C_CUSTOM_EMOJIES} TEXT"
|
||||||
|
|
||||||
// Create conversations table
|
|
||||||
await db.execute("CREATE TABLE ${ConversationTableContract.TABLE_NAME} ("
|
|
||||||
"${ConversationTableContract.C_ID} INTEGER PRIMARY KEY, "
|
|
||||||
"${ConversationTableContract.C_OWNER_ID} INTEGER, "
|
|
||||||
"${ConversationTableContract.C_LAST_ACTIVE} INTEGER, "
|
|
||||||
"${ConversationTableContract.C_NAME} TEXT, "
|
|
||||||
"${ConversationTableContract.C_FOLLOWING} INTEGER, "
|
|
||||||
"${ConversationTableContract.C_SAW_LAST_MESSAGE} INTEGER, "
|
|
||||||
"${ConversationTableContract.C_MEMBERS} TEXT"
|
|
||||||
")");
|
|
||||||
|
|
||||||
// Create conversation messages table
|
|
||||||
await db.execute(
|
|
||||||
"CREATE TABLE ${ConversationsMessagesTableContract.TABLE_NAME} ("
|
|
||||||
"${ConversationsMessagesTableContract.C_ID} INTEGER PRIMARY KEY, "
|
|
||||||
"${ConversationsMessagesTableContract.C_CONVERSATION_ID} INTEGER, "
|
|
||||||
"${ConversationsMessagesTableContract.C_USER_ID} INTEGER, "
|
|
||||||
"${ConversationsMessagesTableContract.C_TIME_INSERT} INTEGER, "
|
|
||||||
"${ConversationsMessagesTableContract.C_MESSAGE} TEXT, "
|
|
||||||
"${ConversationsMessagesTableContract.C_IMAGE_URL} TEXT"
|
|
||||||
")");
|
")");
|
||||||
|
|
||||||
// Friends list table
|
// Friends list table
|
||||||
|
173
lib/helpers/events_helper.dart
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:comunic/models/comment.dart';
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
import 'package:event_bus/event_bus.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
|
||||||
|
/// Events helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
/// Invalid login token
|
||||||
|
class InvalidLoginTokensEvent {}
|
||||||
|
|
||||||
|
/// Main WebSocket closed
|
||||||
|
class WSClosedEvent {}
|
||||||
|
|
||||||
|
/// New number of notifications
|
||||||
|
class NewNumberNotifsEvent {
|
||||||
|
final int newNum;
|
||||||
|
|
||||||
|
NewNumberNotifsEvent(this.newNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// New number of unread conversations
|
||||||
|
class NewNumberUnreadConversations {
|
||||||
|
final int newNum;
|
||||||
|
|
||||||
|
NewNumberUnreadConversations(this.newNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// New comment
|
||||||
|
class NewCommentEvent {
|
||||||
|
final Comment comment;
|
||||||
|
|
||||||
|
NewCommentEvent(this.comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updated comment
|
||||||
|
class UpdatedCommentEvent {
|
||||||
|
final Comment comment;
|
||||||
|
|
||||||
|
UpdatedCommentEvent(this.comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deleted comment
|
||||||
|
class DeletedCommentEvent {
|
||||||
|
final int commentID;
|
||||||
|
|
||||||
|
DeletedCommentEvent(this.commentID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writing message in conversation event
|
||||||
|
class WritingMessageInConversationEvent {
|
||||||
|
final int convID;
|
||||||
|
final int userID;
|
||||||
|
|
||||||
|
WritingMessageInConversationEvent(this.convID, this.userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// New conversation message
|
||||||
|
class NewConversationMessageEvent {
|
||||||
|
final ConversationMessage msg;
|
||||||
|
|
||||||
|
NewConversationMessageEvent(this.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updated conversation message
|
||||||
|
class UpdatedConversationMessageEvent {
|
||||||
|
final ConversationMessage msg;
|
||||||
|
|
||||||
|
UpdatedConversationMessageEvent(this.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deleted conversation message
|
||||||
|
class DeletedConversationMessageEvent {
|
||||||
|
final ConversationMessage msg;
|
||||||
|
|
||||||
|
DeletedConversationMessageEvent(this.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove user from conversation
|
||||||
|
class RemovedUserFromConversationEvent {
|
||||||
|
final int convID;
|
||||||
|
final int userID;
|
||||||
|
|
||||||
|
RemovedUserFromConversationEvent(this.convID, this.userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deleted conversation
|
||||||
|
class DeletedConversationEvent {
|
||||||
|
final int convID;
|
||||||
|
|
||||||
|
DeletedConversationEvent(this.convID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User joined call event
|
||||||
|
class UserJoinedCallEvent {
|
||||||
|
final int callID;
|
||||||
|
final int userID;
|
||||||
|
|
||||||
|
UserJoinedCallEvent(this.callID, this.userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User left call event
|
||||||
|
class UserLeftCallEvent {
|
||||||
|
final int callID;
|
||||||
|
final int userID;
|
||||||
|
|
||||||
|
UserLeftCallEvent(this.callID, this.userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// New call signal event
|
||||||
|
class NewCallSignalEvent {
|
||||||
|
final int callID;
|
||||||
|
final int peerID;
|
||||||
|
final RTCSessionDescription sessionDescription;
|
||||||
|
final RTCIceCandidate candidate;
|
||||||
|
|
||||||
|
const NewCallSignalEvent({
|
||||||
|
this.callID,
|
||||||
|
this.peerID,
|
||||||
|
this.sessionDescription,
|
||||||
|
this.candidate,
|
||||||
|
}) : assert(callID != null),
|
||||||
|
assert(peerID != null),
|
||||||
|
assert(sessionDescription != null || candidate != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call peer ready event
|
||||||
|
class CallPeerReadyEvent {
|
||||||
|
final int callID;
|
||||||
|
final int peerID;
|
||||||
|
|
||||||
|
CallPeerReadyEvent(this.callID, this.peerID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call peer interrupted streaming event
|
||||||
|
class CallPeerInterruptedStreamingEvent {
|
||||||
|
final int callID;
|
||||||
|
final int peerID;
|
||||||
|
|
||||||
|
CallPeerInterruptedStreamingEvent(this.callID, this.peerID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call closed event
|
||||||
|
class CallClosedEvent {
|
||||||
|
final int callID;
|
||||||
|
|
||||||
|
CallClosedEvent(this.callID);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventsHelper {
|
||||||
|
static EventBus _mgr = EventBus();
|
||||||
|
|
||||||
|
/// Listen to event
|
||||||
|
///
|
||||||
|
/// Do not use this method directly. You should instead prefer to use
|
||||||
|
/// [SafeState.listen] to handle safely widgets lifecycle...
|
||||||
|
///
|
||||||
|
/// You can not register to global events
|
||||||
|
static StreamSubscription<T> on<T>(void onData(T event)) {
|
||||||
|
if (T == dynamic) throw Exception("Do not register to all events!");
|
||||||
|
|
||||||
|
final stream = _mgr.on<T>();
|
||||||
|
return stream.listen(onData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Propagate an event
|
||||||
|
static void emit<T>(T event) {
|
||||||
|
_mgr.fire(event);
|
||||||
|
}
|
||||||
|
}
|
@ -28,18 +28,12 @@ class FriendsHelper {
|
|||||||
if (response.code != 200) return null;
|
if (response.code != 200) return null;
|
||||||
|
|
||||||
// Parse and return the list of friends
|
// Parse and return the list of friends
|
||||||
FriendsList list = FriendsList();
|
FriendsList list = FriendsList()
|
||||||
response.getArray().forEach(
|
..addAll(response
|
||||||
(f) => list.add(
|
.getArray()
|
||||||
Friend(
|
.cast<Map<String, dynamic>>()
|
||||||
id: f["ID_friend"],
|
.map(apiToFriend)
|
||||||
accepted: f["accepted"] == 1,
|
.toList());
|
||||||
lastActive: f["time_last_activity"],
|
|
||||||
following: f["following"] == 1,
|
|
||||||
canPostTexts: f["canPostTexts"],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save the list of friends
|
// Save the list of friends
|
||||||
_friendsDatabaseHelper.clearTable();
|
_friendsDatabaseHelper.clearTable();
|
||||||
@ -48,6 +42,17 @@ class FriendsHelper {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn an API entry into a [Friend] object
|
||||||
|
static Friend apiToFriend(Map<String, dynamic> row) {
|
||||||
|
return Friend(
|
||||||
|
id: row["ID_friend"],
|
||||||
|
accepted: row["accepted"] == 1,
|
||||||
|
lastActive: row["time_last_activity"],
|
||||||
|
following: row["following"] == 1,
|
||||||
|
canPostTexts: row["canPostTexts"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the list, either from an online or an offline source
|
/// Get the list, either from an online or an offline source
|
||||||
Future<FriendsList> getList({@required bool online}) async {
|
Future<FriendsList> getList({@required bool online}) async {
|
||||||
if (online)
|
if (online)
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:comunic/lists/group_members_list.dart';
|
||||||
import 'package:comunic/lists/groups_list.dart';
|
import 'package:comunic/lists/groups_list.dart';
|
||||||
|
import 'package:comunic/models/advanced_group_info.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
import 'package:comunic/models/group.dart';
|
import 'package:comunic/models/group.dart';
|
||||||
|
import 'package:comunic/models/group_membership.dart';
|
||||||
|
import 'package:comunic/utils/api_utils.dart';
|
||||||
|
import 'package:comunic/utils/map_utils.dart';
|
||||||
|
|
||||||
/// Groups helper
|
/// Groups helper
|
||||||
///
|
///
|
||||||
@ -34,6 +41,17 @@ const _APIGroupsPostsCreationLevelsMap = {
|
|||||||
|
|
||||||
final _groupsListCache = GroupsList();
|
final _groupsListCache = GroupsList();
|
||||||
|
|
||||||
|
/// Callback for getting advanced user information
|
||||||
|
enum GetAdvancedInfoStatus { SUCCESS, ACCESS_DENIED }
|
||||||
|
|
||||||
|
class GetAdvancedInfoResult {
|
||||||
|
final GetAdvancedInfoStatus status;
|
||||||
|
final AdvancedGroupInfo info;
|
||||||
|
|
||||||
|
GetAdvancedInfoResult(this.status, this.info) : assert(status != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Groups helper
|
||||||
class GroupsHelper {
|
class GroupsHelper {
|
||||||
/// Download a list of groups information from the server
|
/// Download a list of groups information from the server
|
||||||
Future<GroupsList> _downloadList(Set<int> groups) async {
|
Future<GroupsList> _downloadList(Set<int> groups) async {
|
||||||
@ -72,7 +90,7 @@ class GroupsHelper {
|
|||||||
// Check which groups information to download
|
// Check which groups information to download
|
||||||
final toDownload = Set<int>();
|
final toDownload = Set<int>();
|
||||||
groups.forEach((groupID) {
|
groups.forEach((groupID) {
|
||||||
if (_groupsListCache.containsKey(groupID))
|
if (!force && _groupsListCache.containsKey(groupID))
|
||||||
list[groupID] = _groupsListCache[groupID];
|
list[groupID] = _groupsListCache[groupID];
|
||||||
else
|
else
|
||||||
toDownload.add(groupID);
|
toDownload.add(groupID);
|
||||||
@ -91,18 +109,268 @@ class GroupsHelper {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get information about a single group
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
Future<Group> getSingle(int groupID, {bool force = false}) async {
|
||||||
|
return (await getListOrThrow(Set<int>()..add(groupID), force: force))
|
||||||
|
.values
|
||||||
|
.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the list of groups of a user
|
||||||
|
Future<Set<int>> getListUser() async =>
|
||||||
|
(await APIRequest(uri: "groups/get_my_list", needLogin: true).exec())
|
||||||
|
.assertOk()
|
||||||
|
.getArray()
|
||||||
|
.map((f) => cast<int>(f))
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
/// Create a new group
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<int> create(String name) async {
|
||||||
|
final result = await APIRequest.withLogin("groups/create")
|
||||||
|
.addString("name", name)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
return result.getObject()["id"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a simple membership request
|
||||||
|
Future<bool> _simpleMembershipRequest(int groupID, String uri,
|
||||||
|
{Map<String, String> args}) async =>
|
||||||
|
(await (APIRequest(uri: uri, needLogin: true)
|
||||||
|
..addInt("id", groupID)
|
||||||
|
..addArgs(args == null ? Map() : args))
|
||||||
|
.exec())
|
||||||
|
.isOK;
|
||||||
|
|
||||||
|
/// Remove group membership
|
||||||
|
Future<bool> removeMembership(int groupID) async =>
|
||||||
|
_simpleMembershipRequest(groupID, "groups/remove_membership");
|
||||||
|
|
||||||
|
/// Cancel membership request
|
||||||
|
Future<bool> cancelRequest(int groupID) async =>
|
||||||
|
_simpleMembershipRequest(groupID, "groups/cancel_request");
|
||||||
|
|
||||||
|
/// Send a new membership request
|
||||||
|
Future<bool> sendRequest(int groupID) async =>
|
||||||
|
_simpleMembershipRequest(groupID, "groups/send_request");
|
||||||
|
|
||||||
|
/// Respond to a group membership invitation
|
||||||
|
Future<bool> respondInvitation(int groupID, bool accept) async =>
|
||||||
|
_simpleMembershipRequest(groupID, "groups/respond_invitation", args: {
|
||||||
|
"accept": accept ? "true" : "false",
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Update group following status
|
||||||
|
Future<bool> setFollowing(int groupID, bool follow) async =>
|
||||||
|
(await (APIRequest(uri: "groups/set_following", needLogin: true)
|
||||||
|
..addInt("groupID", groupID)
|
||||||
|
..addBool("follow", follow))
|
||||||
|
.exec())
|
||||||
|
.isOK;
|
||||||
|
|
||||||
|
/// Get advanced information about the user
|
||||||
|
Future<GetAdvancedInfoResult> getAdvancedInfo(int groupID) async {
|
||||||
|
// Get advanced information about the user
|
||||||
|
final result =
|
||||||
|
await (APIRequest(uri: "groups/get_advanced_info", needLogin: true)
|
||||||
|
..addInt("id", groupID))
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
switch (result.code) {
|
||||||
|
case 401:
|
||||||
|
return GetAdvancedInfoResult(GetAdvancedInfoStatus.ACCESS_DENIED, null);
|
||||||
|
|
||||||
|
case 200:
|
||||||
|
return GetAdvancedInfoResult(GetAdvancedInfoStatus.SUCCESS,
|
||||||
|
_getAdvancedGroupInfoFromAPI(result.getObject()));
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Exception("Could not get advanced group information!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get group settings
|
||||||
|
///
|
||||||
|
/// This function is currently a kind of alias, but it might
|
||||||
|
/// change in the future
|
||||||
|
///
|
||||||
|
/// Throws in case of error
|
||||||
|
Future<AdvancedGroupInfo> getSettings(int groupID) async {
|
||||||
|
final groupInfo = await getAdvancedInfo(groupID);
|
||||||
|
|
||||||
|
if (groupInfo.status != GetAdvancedInfoStatus.SUCCESS)
|
||||||
|
throw Exception("Could not get group information!");
|
||||||
|
|
||||||
|
return groupInfo.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the availability of a virtual directory
|
||||||
|
///
|
||||||
|
/// Throws in case of error
|
||||||
|
static Future<void> checkVirtualDirectoryAvailability(
|
||||||
|
int groupID, String dir) async =>
|
||||||
|
await APIRequest(uri: "groups/checkVirtualDirectory", needLogin: true)
|
||||||
|
.addInt("groupID", groupID)
|
||||||
|
.addString("directory", dir)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Update (set) new group settings
|
||||||
|
///
|
||||||
|
/// Throws in case of error
|
||||||
|
static Future<void> setSettings(AdvancedGroupInfo settings) async {
|
||||||
|
await APIRequest(uri: "groups/set_settings", needLogin: true)
|
||||||
|
.addInt("id", settings.id)
|
||||||
|
.addString("name", settings.name)
|
||||||
|
.addString("virtual_directory", settings.virtualDirectory)
|
||||||
|
.addString("visibility",
|
||||||
|
invertMap(_APIGroupsVisibilityLevelsMap)[settings.visibilityLevel])
|
||||||
|
.addString(
|
||||||
|
"registration_level",
|
||||||
|
invertMap(
|
||||||
|
_APIGroupsRegistrationLevelsMap)[settings.registrationLevel])
|
||||||
|
.addString(
|
||||||
|
"posts_level",
|
||||||
|
invertMap(
|
||||||
|
_APIGroupsPostsCreationLevelsMap)[settings.postCreationLevel])
|
||||||
|
.addString("description", settings.description)
|
||||||
|
.addString("url", settings.url)
|
||||||
|
.execWithThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a new logo
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> uploadNewLogo(int groupID, Uint8List bytes) async =>
|
||||||
|
await APIRequest(uri: "groups/upload_logo", needLogin: true)
|
||||||
|
.addInt("id", groupID)
|
||||||
|
.addBytesFile("logo", BytesFile("logo.png", bytes))
|
||||||
|
.execWithFilesAndThrow();
|
||||||
|
|
||||||
|
/// Delete group logo
|
||||||
|
///
|
||||||
|
/// Throws in case of error
|
||||||
|
static Future<void> deleteLogo(int groupID) async =>
|
||||||
|
await APIRequest(uri: "groups/delete_logo", needLogin: true)
|
||||||
|
.addInt("id", groupID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Delete a group
|
||||||
|
///
|
||||||
|
/// Throws in case of error
|
||||||
|
static Future<void> deleteGroup(int groupID, String password) async =>
|
||||||
|
await APIRequest(uri: "groups/delete", needLogin: true)
|
||||||
|
.addInt("groupID", groupID)
|
||||||
|
.addString("password", password)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Get the list of members of the group
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<GroupMembersList> getMembersList(int groupID) async =>
|
||||||
|
GroupMembersList()
|
||||||
|
..addAll((await APIRequest(uri: "groups/get_members", needLogin: true)
|
||||||
|
.addInt("id", groupID)
|
||||||
|
.execWithThrow())
|
||||||
|
.getArray()
|
||||||
|
.map((f) => _apiToGroupMembership(f))
|
||||||
|
.toList());
|
||||||
|
|
||||||
|
/// Invite a user to join a group
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
|
static Future<void> sendInvitation(int groupID, int userID) async =>
|
||||||
|
APIRequest.withLogin("groups/invite")
|
||||||
|
.addInt("group_id", groupID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Cancel a group membership invitation
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
|
static Future<void> cancelInvitation(int groupID, int userID) async =>
|
||||||
|
await APIRequest.withLogin("groups/cancel_invitation")
|
||||||
|
.addInt("groupID", groupID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Respond to a group membership request
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
|
static Future<void> respondRequest(
|
||||||
|
int groupID, int userID, bool accept) async =>
|
||||||
|
await APIRequest.withLogin("groups/respond_request")
|
||||||
|
.addInt("groupID", groupID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.addBool("accept", accept)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Remove a member from a group
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
|
static Future<void> removeMemberFromGroup(int groupID, int userID) async =>
|
||||||
|
APIRequest.withLogin("groups/delete_member")
|
||||||
|
.addInt("groupID", groupID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Change the membership level of a member of a group
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
|
static Future<void> setNewLevel(
|
||||||
|
int groupID, int userID, GroupMembershipLevel level) async =>
|
||||||
|
await APIRequest.withLogin("groups/update_membership_level")
|
||||||
|
.addInt("groupID", groupID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.addString("level", invertMap(_APIGroupsMembershipLevelsMap)[level])
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
/// Turn an API entry into a group object
|
/// Turn an API entry into a group object
|
||||||
Group _getGroupFromAPI(Map<String, dynamic> map) {
|
Group _getGroupFromAPI(Map<String, dynamic> map) {
|
||||||
return Group(
|
return Group(
|
||||||
id: map["id"],
|
id: map["id"],
|
||||||
name: map["name"],
|
name: map["name"],
|
||||||
|
iconURL: map["icon_url"],
|
||||||
numberMembers: map["number_members"],
|
numberMembers: map["number_members"],
|
||||||
membershipLevel: _APIGroupsMembershipLevelsMap[map["membership"]],
|
membershipLevel: _APIGroupsMembershipLevelsMap[map["membership"]],
|
||||||
visibilityLevel: _APIGroupsVisibilityLevelsMap[map["visibility"]],
|
visibilityLevel: _APIGroupsVisibilityLevelsMap[map["visibility"]],
|
||||||
registrationLevel:
|
registrationLevel:
|
||||||
_APIGroupsRegistrationLevelsMap[map["registration_level"]],
|
_APIGroupsRegistrationLevelsMap[map["registration_level"]],
|
||||||
postCreationLevel: _APIGroupsPostsCreationLevelsMap[map["posts_level"]],
|
postCreationLevel: _APIGroupsPostsCreationLevelsMap[map["posts_level"]],
|
||||||
virtualDirectory: map["virtual_directory"],
|
virtualDirectory: nullToEmpty(map["virtual_directory"]),
|
||||||
following: map["following"]);
|
following: map["following"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get advanced group information
|
||||||
|
AdvancedGroupInfo _getAdvancedGroupInfoFromAPI(Map<String, dynamic> map) =>
|
||||||
|
AdvancedGroupInfo(
|
||||||
|
id: map["id"],
|
||||||
|
name: map["name"],
|
||||||
|
iconURL: map["icon_url"],
|
||||||
|
numberMembers: map["number_members"],
|
||||||
|
membershipLevel: _APIGroupsMembershipLevelsMap[map["membership"]],
|
||||||
|
visibilityLevel: _APIGroupsVisibilityLevelsMap[map["visibility"]],
|
||||||
|
registrationLevel:
|
||||||
|
_APIGroupsRegistrationLevelsMap[map["registration_level"]],
|
||||||
|
postCreationLevel: _APIGroupsPostsCreationLevelsMap[map["posts_level"]],
|
||||||
|
virtualDirectory: nullToEmpty(map["virtual_directory"]),
|
||||||
|
following: map["following"],
|
||||||
|
timeCreate: map["time_create"],
|
||||||
|
description: nullToEmpty(map["description"]),
|
||||||
|
url: nullToEmpty(map["url"]),
|
||||||
|
likes: map["number_likes"],
|
||||||
|
userLike: map["is_liking"],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Create [GroupMembership] object from API entry
|
||||||
|
static GroupMembership _apiToGroupMembership(Map<String, dynamic> row) =>
|
||||||
|
GroupMembership(
|
||||||
|
userID: row["user_id"],
|
||||||
|
groupID: row["group_id"],
|
||||||
|
timeCreate: row["time_create"],
|
||||||
|
level: _APIGroupsMembershipLevelsMap[row["level"]],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:comunic/enums/likes_type.dart';
|
import 'package:comunic/enums/likes_type.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/helpers/websocket_helper.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
/// Likes helper
|
/// Likes helper
|
||||||
@ -15,21 +15,15 @@ const LikesAPIMap = {
|
|||||||
|
|
||||||
class LikesHelper {
|
class LikesHelper {
|
||||||
/// Update liking status of an element
|
/// Update liking status of an element
|
||||||
Future<bool> setLiking({
|
Future<void> setLiking({
|
||||||
@required LikesType type,
|
@required LikesType type,
|
||||||
@required bool like,
|
@required bool like,
|
||||||
@required int id,
|
@required int id,
|
||||||
}) async {
|
}) async {
|
||||||
return (await APIRequest(
|
return (await ws("likes/update", {
|
||||||
uri: "likes/update",
|
"type": LikesAPIMap[type],
|
||||||
needLogin: true,
|
"like": like.toString(),
|
||||||
args: {
|
"id": id.toString(),
|
||||||
"type": LikesAPIMap[type],
|
}));
|
||||||
"like": like.toString(),
|
|
||||||
"id": id.toString(),
|
|
||||||
},
|
|
||||||
).exec())
|
|
||||||
.code ==
|
|
||||||
200;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:comunic/lists/notifications_list.dart';
|
import 'package:comunic/lists/notifications_list.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/count_unread_notifications.dart';
|
||||||
import 'package:comunic/models/notification.dart';
|
import 'package:comunic/models/notification.dart';
|
||||||
|
|
||||||
/// Notifications helper
|
/// Notifications helper
|
||||||
@ -15,7 +16,6 @@ const _NotificationElementTypeAPImapping = {
|
|||||||
"post_text": NotificationElementType.POST_TEXT,
|
"post_text": NotificationElementType.POST_TEXT,
|
||||||
"post_img": NotificationElementType.POST_IMAGE,
|
"post_img": NotificationElementType.POST_IMAGE,
|
||||||
"post_youtube": NotificationElementType.POST_YOUTUBE,
|
"post_youtube": NotificationElementType.POST_YOUTUBE,
|
||||||
"post_movie": NotificationElementType.POST_MOVIE,
|
|
||||||
"post_weblink": NotificationElementType.POST_WEBLINK,
|
"post_weblink": NotificationElementType.POST_WEBLINK,
|
||||||
"post_pdf": NotificationElementType.POST_PDF,
|
"post_pdf": NotificationElementType.POST_PDF,
|
||||||
"post_timer": NotificationElementType.POST_TIMER,
|
"post_timer": NotificationElementType.POST_TIMER,
|
||||||
@ -47,6 +47,22 @@ const _NotificationsTypeAPImapping = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class NotificationsHelper {
|
class NotificationsHelper {
|
||||||
|
/// Get the number of unread notifications
|
||||||
|
///
|
||||||
|
/// This method throws in case of error
|
||||||
|
Future<CountUnreadNotifications> countUnread() async {
|
||||||
|
final response =
|
||||||
|
await APIRequest(uri: "notifications/count_all_news", needLogin: true)
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
final content = response.assertOk().getObject();
|
||||||
|
|
||||||
|
return CountUnreadNotifications(
|
||||||
|
notifications: content["notifications"],
|
||||||
|
conversations: content["conversations"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the list of unread notifications of the user
|
/// Get the list of unread notifications of the user
|
||||||
Future<NotificationsList> getUnread() async {
|
Future<NotificationsList> getUnread() async {
|
||||||
final response =
|
final response =
|
||||||
@ -85,4 +101,10 @@ class NotificationsHelper {
|
|||||||
},
|
},
|
||||||
).exec())
|
).exec())
|
||||||
.isOK;
|
.isOK;
|
||||||
|
|
||||||
|
/// Delete all unread notifications
|
||||||
|
Future<bool> deleteAllNotifications() async =>
|
||||||
|
(await APIRequest(uri: "notifications/delete_all", needLogin: true)
|
||||||
|
.exec())
|
||||||
|
.isOK;
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,14 @@ import 'package:comunic/enums/post_visibility_level.dart';
|
|||||||
import 'package:comunic/enums/user_access_levels.dart';
|
import 'package:comunic/enums/user_access_levels.dart';
|
||||||
import 'package:comunic/helpers/comments_helper.dart';
|
import 'package:comunic/helpers/comments_helper.dart';
|
||||||
import 'package:comunic/helpers/survey_helper.dart';
|
import 'package:comunic/helpers/survey_helper.dart';
|
||||||
|
import 'package:comunic/helpers/websocket_helper.dart';
|
||||||
import 'package:comunic/lists/comments_list.dart';
|
import 'package:comunic/lists/comments_list.dart';
|
||||||
import 'package:comunic/lists/posts_list.dart';
|
import 'package:comunic/lists/posts_list.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/displayed_content.dart';
|
||||||
import 'package:comunic/models/new_post.dart';
|
import 'package:comunic/models/new_post.dart';
|
||||||
import 'package:comunic/models/post.dart';
|
import 'package:comunic/models/post.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
/// Posts helper
|
/// Posts helper
|
||||||
///
|
///
|
||||||
@ -26,7 +29,6 @@ const _APIPostsKindsMap = {
|
|||||||
"image": PostKind.IMAGE,
|
"image": PostKind.IMAGE,
|
||||||
"weblink": PostKind.WEB_LINK,
|
"weblink": PostKind.WEB_LINK,
|
||||||
"pdf": PostKind.PDF,
|
"pdf": PostKind.PDF,
|
||||||
"movie": PostKind.MOVIE,
|
|
||||||
"countdown": PostKind.COUNTDOWN,
|
"countdown": PostKind.COUNTDOWN,
|
||||||
"survey": PostKind.SURVEY,
|
"survey": PostKind.SURVEY,
|
||||||
"youtube": PostKind.YOUTUBE
|
"youtube": PostKind.YOUTUBE
|
||||||
@ -45,6 +47,12 @@ const _APIPostsTargetKindsMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class PostsHelper {
|
class PostsHelper {
|
||||||
|
/// Stores the list of posts we are registered to
|
||||||
|
///
|
||||||
|
/// First int = post ID
|
||||||
|
/// Second int = number of registered people
|
||||||
|
static final _registeredPosts = Map<int, int>();
|
||||||
|
|
||||||
/// Get the list of latest posts. Return the list of posts or null in case of
|
/// Get the list of latest posts. Return the list of posts or null in case of
|
||||||
/// failure
|
/// failure
|
||||||
Future<PostsList> getLatest({int from = 0}) async {
|
Future<PostsList> getLatest({int from = 0}) async {
|
||||||
@ -67,10 +75,27 @@ class PostsHelper {
|
|||||||
|
|
||||||
/// Get the list of posts of a user
|
/// Get the list of posts of a user
|
||||||
Future<PostsList> getUserPosts(int userID, {int from = 0}) async {
|
Future<PostsList> getUserPosts(int userID, {int from = 0}) async {
|
||||||
final response = await APIRequest(
|
final response = await (APIRequest(uri: "posts/get_user", needLogin: true)
|
||||||
uri: "posts/get_user",
|
..addInt("userID", userID)
|
||||||
needLogin: true,
|
..addInt("startFrom", from == 0 ? 0 : from - 1))
|
||||||
args: {"userID": userID.toString(), "startFrom": from.toString()})
|
.exec();
|
||||||
|
|
||||||
|
if (response.code != 200) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse & return the list of posts
|
||||||
|
return PostsList()..addAll(response.getArray().map((f) => _apiToPost(f)));
|
||||||
|
} catch (e) {
|
||||||
|
print(e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the list of posts of a group
|
||||||
|
Future<PostsList> getGroupPosts(int groupID, {int from = 0}) async {
|
||||||
|
final response = await (APIRequest(uri: "posts/get_group", needLogin: true)
|
||||||
|
..addInt("groupID", groupID)
|
||||||
|
..addInt("startFrom", from == 0 ? 0 : from - 1))
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
if (response.code != 200) return null;
|
if (response.code != 200) return null;
|
||||||
@ -117,7 +142,33 @@ class PostsHelper {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case PostKind.IMAGE:
|
case PostKind.IMAGE:
|
||||||
request.addFile("image", post.image);
|
request.addBytesFile("image", post.image);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PostKind.WEB_LINK:
|
||||||
|
request.addString("url", post.url);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PostKind.PDF:
|
||||||
|
request.addBytesFile(
|
||||||
|
"pdf",
|
||||||
|
BytesFile("file.pdf", post.pdf,
|
||||||
|
type: MediaType.parse("application/pdf")));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PostKind.COUNTDOWN:
|
||||||
|
request.addInt(
|
||||||
|
"time-end", (post.timeEnd.millisecondsSinceEpoch / 1000).floor());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PostKind.SURVEY:
|
||||||
|
request.addString("question", post.survey.question);
|
||||||
|
request.addString("answers", post.survey.answers.join("<>"));
|
||||||
|
request.addBool("allowNewAnswers", post.survey.allowNewChoicesCreation);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PostKind.YOUTUBE:
|
||||||
|
request.addString("youtube_id", post.youtubeId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -167,6 +218,28 @@ class PostsHelper {
|
|||||||
.isOK;
|
.isOK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register to a post events
|
||||||
|
Future<void> registerPostEvents(int id) async {
|
||||||
|
if (_registeredPosts.containsKey(id))
|
||||||
|
_registeredPosts[id]++;
|
||||||
|
else {
|
||||||
|
_registeredPosts[id] = 1;
|
||||||
|
await ws("\$main/register_post", {"postID": id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un-register to post events
|
||||||
|
Future<void> unregisterPostEvents(int id) async {
|
||||||
|
if (!_registeredPosts.containsKey(id)) return;
|
||||||
|
|
||||||
|
_registeredPosts[id]--;
|
||||||
|
|
||||||
|
if (_registeredPosts[id] <= 0) {
|
||||||
|
_registeredPosts.remove(id);
|
||||||
|
await ws("\$main/unregister_post", {"postID": id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Turn an API entry into a [Post] object
|
/// Turn an API entry into a [Post] object
|
||||||
Post _apiToPost(Map<String, dynamic> map) {
|
Post _apiToPost(Map<String, dynamic> map) {
|
||||||
final postKind = _APIPostsKindsMap[map["kind"]];
|
final postKind = _APIPostsKindsMap[map["kind"]];
|
||||||
@ -189,7 +262,7 @@ class PostsHelper {
|
|||||||
userPageID: map["user_page_id"],
|
userPageID: map["user_page_id"],
|
||||||
groupID: map["group_id"],
|
groupID: map["group_id"],
|
||||||
timeSent: map["post_time"],
|
timeSent: map["post_time"],
|
||||||
content: map["content"],
|
content: DisplayedString(map["content"]),
|
||||||
visibilityLevel: _APIPostsVisibilityLevelMap[map["visibility_level"]],
|
visibilityLevel: _APIPostsVisibilityLevelMap[map["visibility_level"]],
|
||||||
kind: postKind,
|
kind: postKind,
|
||||||
fileSize: map["file_size"],
|
fileSize: map["file_size"],
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'package:comunic/models/application_preferences.dart';
|
||||||
|
|
||||||
import 'package:comunic/models/login_tokens.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
/// Preferences helper
|
/// Preferences helper
|
||||||
@ -9,11 +7,20 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
enum PreferencesKeyList { LOGIN_TOKENS, ENABLE_DARK_THEME }
|
enum PreferencesKeyList {
|
||||||
|
USER_ID,
|
||||||
|
LOGIN_TOKEN,
|
||||||
|
ENABLE_DARK_THEME,
|
||||||
|
FORCE_MOBILE_MODE,
|
||||||
|
SHOW_PERFORMANCE_OVERLAY,
|
||||||
|
}
|
||||||
|
|
||||||
const _PreferenceKeysName = {
|
const _PreferenceKeysName = {
|
||||||
PreferencesKeyList.LOGIN_TOKENS: "login_tokens",
|
PreferencesKeyList.USER_ID: "user_id",
|
||||||
|
PreferencesKeyList.LOGIN_TOKEN: "login_token",
|
||||||
PreferencesKeyList.ENABLE_DARK_THEME: "dark_theme",
|
PreferencesKeyList.ENABLE_DARK_THEME: "dark_theme",
|
||||||
|
PreferencesKeyList.FORCE_MOBILE_MODE: "force_mobile_mode",
|
||||||
|
PreferencesKeyList.SHOW_PERFORMANCE_OVERLAY: "perfs_overlay",
|
||||||
};
|
};
|
||||||
|
|
||||||
class PreferencesHelper {
|
class PreferencesHelper {
|
||||||
@ -37,23 +44,28 @@ class PreferencesHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set new login tokens
|
/// Set new login tokens
|
||||||
Future<void> setLoginTokens(LoginTokens tokens) async {
|
Future<void> setLoginToken(String token) async {
|
||||||
await setString(PreferencesKeyList.LOGIN_TOKENS,
|
if (token != null)
|
||||||
tokens == null ? "null" : tokens.toString());
|
await setString(PreferencesKeyList.LOGIN_TOKEN, token);
|
||||||
|
else
|
||||||
|
await removeKey(PreferencesKeyList.LOGIN_TOKEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get current [LoginTokens]. Returns null if none or in case of failure
|
/// Get current [LoginTokens]. Returns null if none or in case of failure
|
||||||
LoginTokens getLoginTokens() {
|
String getLoginToken() {
|
||||||
try {
|
try {
|
||||||
final string = getString(PreferencesKeyList.LOGIN_TOKENS);
|
final string = getString(PreferencesKeyList.LOGIN_TOKEN);
|
||||||
if (string == null || string == "null") return null;
|
return string;
|
||||||
return LoginTokens.fromJSON(jsonDecode(string));
|
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> removeKey(PreferencesKeyList key) async {
|
||||||
|
return await _sharedPreferences.remove(_PreferenceKeysName[key]);
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> setString(PreferencesKeyList key, String value) async {
|
Future<bool> setString(PreferencesKeyList key, String value) async {
|
||||||
return await _sharedPreferences.setString(_PreferenceKeysName[key], value);
|
return await _sharedPreferences.setString(_PreferenceKeysName[key], value);
|
||||||
}
|
}
|
||||||
@ -66,10 +78,31 @@ class PreferencesHelper {
|
|||||||
return await _sharedPreferences.setBool(_PreferenceKeysName[key], value);
|
return await _sharedPreferences.setBool(_PreferenceKeysName[key], value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> setInt(PreferencesKeyList key, int value) async {
|
||||||
|
return await _sharedPreferences.setInt(_PreferenceKeysName[key], value);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getInt(PreferencesKeyList key) {
|
||||||
|
return _sharedPreferences.getInt(_PreferenceKeysName[key]);
|
||||||
|
}
|
||||||
|
|
||||||
bool getBool(PreferencesKeyList key, {bool alternative = false}) {
|
bool getBool(PreferencesKeyList key, {bool alternative = false}) {
|
||||||
final v = _sharedPreferences.getBool(_PreferenceKeysName[key]);
|
final v = _sharedPreferences.getBool(_PreferenceKeysName[key]);
|
||||||
return v == null ? alternative : v;
|
return v == null ? alternative : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all settings as an [ApplicationPreferences] object
|
||||||
|
ApplicationPreferences get preferences => ApplicationPreferences(
|
||||||
|
enableDarkMode: getBool(PreferencesKeyList.ENABLE_DARK_THEME),
|
||||||
|
forceMobileMode: getBool(PreferencesKeyList.FORCE_MOBILE_MODE),
|
||||||
|
showPerformancesOverlay:
|
||||||
|
getBool(PreferencesKeyList.SHOW_PERFORMANCE_OVERLAY));
|
||||||
|
|
||||||
|
/// Apply new preferences
|
||||||
|
Future<void> setPreferences(ApplicationPreferences preferences) async {
|
||||||
|
await setBool(
|
||||||
|
PreferencesKeyList.ENABLE_DARK_THEME, preferences.enableDarkMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PreferencesHelper preferences() {
|
PreferencesHelper preferences() {
|
||||||
|
@ -1,31 +1,47 @@
|
|||||||
import 'package:comunic/helpers/users_helper.dart';
|
import 'package:comunic/helpers/users_helper.dart';
|
||||||
|
import 'package:comunic/lists/search_results_list.dart';
|
||||||
import 'package:comunic/lists/users_list.dart';
|
import 'package:comunic/lists/users_list.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
import 'package:comunic/utils/list_utils.dart';
|
import 'package:comunic/models/search_result.dart';
|
||||||
|
import 'package:comunic/utils/api_utils.dart';
|
||||||
|
|
||||||
/// Search helper
|
/// Search helper
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class SearchHelper {
|
class SearchHelper {
|
||||||
|
|
||||||
/// Search for user. This method returns information about the target users
|
/// Search for user. This method returns information about the target users
|
||||||
///
|
///
|
||||||
/// Returns information about the target users or null if an error occurred
|
/// Returns information about the target users or null if an error occurred
|
||||||
Future<UsersList> searchUser(String query) async {
|
Future<UsersList> searchUser(String query) async {
|
||||||
// Execute the query on the server
|
// Execute the query on the server
|
||||||
final response = await APIRequest(
|
final response = await APIRequest(
|
||||||
uri: "user/search",
|
uri: "user/search", needLogin: true, args: {"query": query}).exec();
|
||||||
needLogin: true,
|
|
||||||
args: {
|
|
||||||
"query": query
|
|
||||||
}
|
|
||||||
).exec();
|
|
||||||
|
|
||||||
if (response.code != 200) return null;
|
if (response.code != 200) return null;
|
||||||
|
|
||||||
return await UsersHelper().getUsersInfo(
|
return await UsersHelper()
|
||||||
listToIntList(response.getArray()));
|
.getUsersInfo(response.getArray().map((f) => cast<int>(f)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/// Perform a global search
|
||||||
|
Future<SearchResultsList> globalSearch(String query) async {
|
||||||
|
final result = await APIRequest(
|
||||||
|
uri: "search/global", needLogin: true, args: {"query": query}).exec();
|
||||||
|
|
||||||
|
result.assertOk();
|
||||||
|
|
||||||
|
return SearchResultsList()..addAll(result.getArray().map((f) {
|
||||||
|
switch (f["kind"]) {
|
||||||
|
case "user":
|
||||||
|
return SearchResult(id: f["id"], kind: SearchResultKind.USER);
|
||||||
|
|
||||||
|
case "group":
|
||||||
|
return SearchResult(id: f["id"], kind: SearchResultKind.GROUP);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Exception("Unkown search kind: ${f["kind"]}");
|
||||||
|
}
|
||||||
|
}).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
121
lib/helpers/serialization/base_serialization_helper.dart
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:comunic/constants.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
/// Base serialization helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
abstract class SerializableElement<T> extends Comparable<T> {
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseSerializationHelper<T extends SerializableElement> {
|
||||||
|
/// List cache
|
||||||
|
List<T> _cache;
|
||||||
|
|
||||||
|
/// The name of the type of data to serialise
|
||||||
|
String get type;
|
||||||
|
|
||||||
|
/// Parse an json entry into a [T] object
|
||||||
|
T parse(Map<String, dynamic> m);
|
||||||
|
|
||||||
|
/// Get the file where data should be stored
|
||||||
|
Future<File> _getFilePath() async {
|
||||||
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
final targetDir =
|
||||||
|
Directory(path.join(dir.absolute.path, SERIALIZATION_DIRECTORY));
|
||||||
|
|
||||||
|
targetDir.create(recursive: true);
|
||||||
|
|
||||||
|
return File(path.join(targetDir.absolute.path, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the cache
|
||||||
|
Future<void> _loadCache() async {
|
||||||
|
if (_cache != null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final file = await _getFilePath();
|
||||||
|
|
||||||
|
if (!await file.exists()) return _cache = [];
|
||||||
|
|
||||||
|
final List<dynamic> json = jsonDecode(await file.readAsString());
|
||||||
|
_cache = json.cast<Map<String, dynamic>>().map(parse).toList();
|
||||||
|
|
||||||
|
_cache.sort();
|
||||||
|
} catch (e, s) {
|
||||||
|
print("Failed to read serialized data! $e => $s");
|
||||||
|
_cache = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the cache to the persistent memory
|
||||||
|
Future<void> _saveCache() async {
|
||||||
|
final file = await _getFilePath();
|
||||||
|
await file.writeAsString(jsonEncode(
|
||||||
|
_cache.map((e) => e.toJson()).toList().cast<Map<String, dynamic>>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current list of elements
|
||||||
|
Future<List<T>> getList() async {
|
||||||
|
await _loadCache();
|
||||||
|
return List.from(_cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a new list of conversations
|
||||||
|
Future<void> setList(List<T> list) async {
|
||||||
|
_cache = List.from(list);
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert new element
|
||||||
|
Future<void> insert(T el) async {
|
||||||
|
await _loadCache();
|
||||||
|
_cache.add(el);
|
||||||
|
_cache.sort();
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert new element
|
||||||
|
Future<void> insertMany(List<T> els) async {
|
||||||
|
await _loadCache();
|
||||||
|
_cache.addAll(els);
|
||||||
|
_cache.sort();
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if any entry in the last match the predicate
|
||||||
|
Future<bool> any(bool isContained(T t)) async {
|
||||||
|
await _loadCache();
|
||||||
|
return _cache.any((element) => isContained(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if any entry in the last match the predicate
|
||||||
|
Future<T> first(bool filter(T t)) async {
|
||||||
|
await _loadCache();
|
||||||
|
return _cache.firstWhere((element) => filter(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace an element with another one
|
||||||
|
Future<void> insertOrReplaceElement(bool isToReplace(T t), T newEl) async {
|
||||||
|
await _loadCache();
|
||||||
|
|
||||||
|
// Insert or replace the element
|
||||||
|
_cache.where((element) => !isToReplace(element)).toList();
|
||||||
|
_cache.add(newEl);
|
||||||
|
|
||||||
|
_cache.sort();
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove elements
|
||||||
|
Future<void> removeElement(bool isToRemove(T t)) async {
|
||||||
|
await _loadCache();
|
||||||
|
_cache.removeWhere((element) => isToRemove(element));
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
|
||||||
|
import 'package:comunic/lists/conversation_messages_list.dart';
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
|
||||||
|
/// Conversations serialization helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
HashMap<int, ConversationsMessagesSerializationHelper> _instances;
|
||||||
|
|
||||||
|
class ConversationsMessagesSerializationHelper
|
||||||
|
extends BaseSerializationHelper<ConversationMessage> {
|
||||||
|
final int convID;
|
||||||
|
|
||||||
|
ConversationsMessagesSerializationHelper._(int convID)
|
||||||
|
: convID = convID,
|
||||||
|
assert(convID != null);
|
||||||
|
|
||||||
|
factory ConversationsMessagesSerializationHelper(int convID) {
|
||||||
|
if (_instances == null) _instances = HashMap();
|
||||||
|
|
||||||
|
if (!_instances.containsKey(convID))
|
||||||
|
_instances[convID] = ConversationsMessagesSerializationHelper._(convID);
|
||||||
|
|
||||||
|
return _instances[convID];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConversationMessage parse(Map<String, dynamic> m) =>
|
||||||
|
ConversationMessage.fromJson(m);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get type => "conv-messages-$convID";
|
||||||
|
|
||||||
|
Future<ConversationMessagesList> getList() async =>
|
||||||
|
ConversationMessagesList()..addAll(await super.getList());
|
||||||
|
|
||||||
|
Future<void> insertOrReplace(ConversationMessage msg) async =>
|
||||||
|
await insertOrReplaceElement((t) => t.id == msg.id, msg);
|
||||||
|
|
||||||
|
Future<void> remove(ConversationMessage msg) async =>
|
||||||
|
await removeElement((t) => t.id == msg.id);
|
||||||
|
|
||||||
|
/// Insert or replace a list of messages
|
||||||
|
Future<void> insertOrReplaceAll(List<ConversationMessage> list) async {
|
||||||
|
for (var message in list)
|
||||||
|
await insertOrReplaceElement((t) => t.id == message.id, message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
|
||||||
|
import 'package:comunic/lists/conversations_list.dart';
|
||||||
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
|
||||||
|
/// Conversations serialization helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
var _cache;
|
||||||
|
|
||||||
|
class ConversationsSerializationHelper
|
||||||
|
extends BaseSerializationHelper<Conversation> {
|
||||||
|
/// Singleton
|
||||||
|
factory ConversationsSerializationHelper() {
|
||||||
|
if (_cache == null) _cache = ConversationsSerializationHelper._();
|
||||||
|
return _cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConversationsSerializationHelper._();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Conversation parse(Map<String, dynamic> m) => Conversation.fromJson(m);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get type => "conversations";
|
||||||
|
|
||||||
|
Future<ConversationsList> getList() async =>
|
||||||
|
ConversationsList()..addAll(await super.getList());
|
||||||
|
|
||||||
|
/// Get a conversation
|
||||||
|
Future<Conversation> get(int id) => first((t) => t.id == id);
|
||||||
|
}
|
83
lib/helpers/server_config_helper.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/server_config.dart';
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
/// Server configuration helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class ServerConfigurationHelper {
|
||||||
|
static ServerConfig _config;
|
||||||
|
|
||||||
|
/// Make sure the configuration has been correctly loaded
|
||||||
|
static Future<void> ensureLoaded() async {
|
||||||
|
if (_config != null) return;
|
||||||
|
|
||||||
|
final response =
|
||||||
|
(await APIRequest.withoutLogin("server/config").execWithThrow())
|
||||||
|
.getObject();
|
||||||
|
|
||||||
|
final passwordPolicy = response["password_policy"];
|
||||||
|
final dataConservationPolicy = response["data_conservation_policy"];
|
||||||
|
final conversationsPolicy = response["conversations_policy"];
|
||||||
|
|
||||||
|
_config = ServerConfig(
|
||||||
|
minSupportedMobileVersion:
|
||||||
|
Version.parse(response["min_supported_mobile_version"]),
|
||||||
|
termsURL: response["terms_url"],
|
||||||
|
playStoreURL: response["play_store_url"],
|
||||||
|
androidDirectDownloadURL: response["android_direct_download_url"],
|
||||||
|
passwordPolicy: PasswordPolicy(
|
||||||
|
allowMailInPassword: passwordPolicy["allow_email_in_password"],
|
||||||
|
allowNameInPassword: passwordPolicy["allow_name_in_password"],
|
||||||
|
minPasswordLength: passwordPolicy["min_password_length"],
|
||||||
|
minNumberUpperCaseLetters:
|
||||||
|
passwordPolicy["min_number_upper_case_letters"],
|
||||||
|
minNumberLowerCaseLetters:
|
||||||
|
passwordPolicy["min_number_lower_case_letters"],
|
||||||
|
minNumberDigits: passwordPolicy["min_number_digits"],
|
||||||
|
minNumberSpecialCharacters:
|
||||||
|
passwordPolicy["min_number_special_characters"],
|
||||||
|
minCategoriesPresence: passwordPolicy["min_categories_presence"],
|
||||||
|
),
|
||||||
|
dataConservationPolicy: ServerDataConservationPolicy(
|
||||||
|
minInactiveAccountLifetime:
|
||||||
|
dataConservationPolicy["min_inactive_account_lifetime"],
|
||||||
|
minNotificationLifetime:
|
||||||
|
dataConservationPolicy["min_notification_lifetime"],
|
||||||
|
minCommentsLifetime: dataConservationPolicy["min_comments_lifetime"],
|
||||||
|
minPostsLifetime: dataConservationPolicy["min_posts_lifetime"],
|
||||||
|
minConversationMessagesLifetime:
|
||||||
|
dataConservationPolicy["min_conversation_messages_lifetime"],
|
||||||
|
minLikesLifetime: dataConservationPolicy["min_likes_lifetime"],
|
||||||
|
),
|
||||||
|
conversationsPolicy: ConversationsPolicy(
|
||||||
|
minMessageLen: conversationsPolicy["min_message_len"],
|
||||||
|
maxMessageLen: conversationsPolicy["max_message_len"],
|
||||||
|
allowedFilesType:
|
||||||
|
conversationsPolicy["allowed_files_type"].cast<String>(),
|
||||||
|
filesMaxSize: conversationsPolicy["files_max_size"],
|
||||||
|
writingEventInterval: conversationsPolicy["writing_event_interval"],
|
||||||
|
writingEventLifetime: conversationsPolicy["writing_event_lifetime"],
|
||||||
|
maxMessageImageWidth: conversationsPolicy["max_message_image_width"],
|
||||||
|
maxMessageImageHeight:
|
||||||
|
conversationsPolicy["max_message_image_height"],
|
||||||
|
maxThumbnailWidth: conversationsPolicy["max_thumbnail_width"],
|
||||||
|
maxThumbnailHeight: conversationsPolicy["max_thumbnail_height"],
|
||||||
|
maxLogoWidth: conversationsPolicy["max_logo_width"],
|
||||||
|
maxLogoHeight: conversationsPolicy["max_logo_height"],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current server configuration, throwing if it is not loaded yet
|
||||||
|
static ServerConfig get config {
|
||||||
|
if (_config == null)
|
||||||
|
throw Exception(
|
||||||
|
"Trying to access server configuration but it is not loaded yet!");
|
||||||
|
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shortcut for server configuration
|
||||||
|
ServerConfig get srvConfig => ServerConfigurationHelper.config;
|
235
lib/helpers/settings_helper.dart
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import 'package:comunic/enums/user_page_visibility.dart';
|
||||||
|
import 'package:comunic/models/account_image_settings.dart';
|
||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/data_conservation_policy_settings.dart';
|
||||||
|
import 'package:comunic/models/general_settings.dart';
|
||||||
|
import 'package:comunic/models/new_emoji.dart';
|
||||||
|
import 'package:comunic/models/security_settings.dart';
|
||||||
|
|
||||||
|
import '../models/api_request.dart';
|
||||||
|
|
||||||
|
/// Settings helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
const _APIAccountImageVisibilityAPILevels = {
|
||||||
|
"open": AccountImageVisibilityLevels.EVERYONE,
|
||||||
|
"public": AccountImageVisibilityLevels.COMUNIC_USERS,
|
||||||
|
"friends": AccountImageVisibilityLevels.FRIENDS_ONLY,
|
||||||
|
};
|
||||||
|
|
||||||
|
class SettingsHelper {
|
||||||
|
/// Get & return general user settings
|
||||||
|
static Future<GeneralSettings> getGeneralSettings() async {
|
||||||
|
final response =
|
||||||
|
(await APIRequest(uri: "settings/get_general", needLogin: true).exec())
|
||||||
|
.assertOk()
|
||||||
|
.getObject();
|
||||||
|
|
||||||
|
return GeneralSettings(
|
||||||
|
email: response["email"],
|
||||||
|
firstName: response["firstName"],
|
||||||
|
lastName: response["lastName"],
|
||||||
|
pageVisibility: response["is_open"]
|
||||||
|
? UserPageVisibility.OPEN
|
||||||
|
: response["is_public"]
|
||||||
|
? UserPageVisibility.PUBLIC
|
||||||
|
: UserPageVisibility.PRIVATE,
|
||||||
|
allowComments: response["allow_comments"],
|
||||||
|
allowPostsFromFriends: response["allow_posts_from_friends"],
|
||||||
|
allowComunicEmails: response["allow_comunic_mails"],
|
||||||
|
publicFriendsList: response["public_friends_list"],
|
||||||
|
virtualDirectory: response["virtual_directory"],
|
||||||
|
personalWebsite: response["personnal_website"],
|
||||||
|
publicNote: response["publicNote"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply new general settings
|
||||||
|
static Future<void> updateGeneralSettings(GeneralSettings settings) async {
|
||||||
|
(await APIRequest(uri: "settings/set_general", needLogin: true, args: {
|
||||||
|
"firstName": settings.firstName,
|
||||||
|
"lastName": settings.lastName,
|
||||||
|
"allow_comunic_mails": settings.allowComunicEmails ? "true" : "false",
|
||||||
|
"isPublic": settings.pageVisibility != UserPageVisibility.PRIVATE
|
||||||
|
? "true"
|
||||||
|
: "false",
|
||||||
|
"isOpen":
|
||||||
|
settings.pageVisibility == UserPageVisibility.OPEN ? "true" : "false",
|
||||||
|
"allowComments": settings.allowComments ? "true" : "false",
|
||||||
|
"allowPostsFromFriends":
|
||||||
|
settings.allowPostsFromFriends ? "true" : "false",
|
||||||
|
"publicFriendsList": settings.publicFriendsList ? "true" : "false",
|
||||||
|
"personnalWebsite": settings.personalWebsite,
|
||||||
|
"virtualDirectory": settings.virtualDirectory,
|
||||||
|
"publicNote": settings.publicNote,
|
||||||
|
}).exec())
|
||||||
|
.assertOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check out whether a virtual directory is available for a user or not
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> checkUserDirectoryAvailability(String dir) async =>
|
||||||
|
(await APIRequest(
|
||||||
|
uri: "settings/check_user_directory_availability",
|
||||||
|
needLogin: true,
|
||||||
|
args: {"directory": dir}).exec())
|
||||||
|
.assertOk();
|
||||||
|
|
||||||
|
/// Get & return account image settings
|
||||||
|
static Future<AccountImageSettings> getAccountImageSettings() async {
|
||||||
|
final response =
|
||||||
|
(await APIRequest(uri: "settings/get_account_image", needLogin: true)
|
||||||
|
.exec())
|
||||||
|
.assertOk()
|
||||||
|
.getObject();
|
||||||
|
|
||||||
|
return AccountImageSettings(
|
||||||
|
hasImage: response["has_image"],
|
||||||
|
imageURL: response["image_url"],
|
||||||
|
visibility:
|
||||||
|
_APIAccountImageVisibilityAPILevels[response["visibility"]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a new account image
|
||||||
|
static Future<void> uploadAccountImage(BytesFile newImage) async =>
|
||||||
|
await APIRequest(uri: "settings/upload_account_image", needLogin: true)
|
||||||
|
.addBytesFile("picture", newImage)
|
||||||
|
.execWithFilesAndThrow();
|
||||||
|
|
||||||
|
/// Upload a new account image from memory
|
||||||
|
static Future<bool> uploadAccountImageFromMemory(List<int> bytes) async =>
|
||||||
|
(await APIRequest(uri: "settings/upload_account_image", needLogin: true)
|
||||||
|
.addBytesFile("picture", BytesFile("accountImage.png", bytes))
|
||||||
|
.execWithFiles())
|
||||||
|
.isOK;
|
||||||
|
|
||||||
|
/// Change account image visibility level
|
||||||
|
static Future<bool> setAccountImageVisibilityLevel(
|
||||||
|
AccountImageVisibilityLevels level) async =>
|
||||||
|
(await APIRequest(
|
||||||
|
uri: "settings/set_account_image_visibility", needLogin: true)
|
||||||
|
.addString(
|
||||||
|
"visibility",
|
||||||
|
|
||||||
|
// Find appropriate visibility level
|
||||||
|
_APIAccountImageVisibilityAPILevels.entries
|
||||||
|
.firstWhere((f) => f.value == level)
|
||||||
|
.key)
|
||||||
|
.exec())
|
||||||
|
.isOK;
|
||||||
|
|
||||||
|
/// Delete user account image
|
||||||
|
static Future<bool> deleteAccountImage() async =>
|
||||||
|
(await APIRequest(uri: "settings/delete_account_image", needLogin: true)
|
||||||
|
.exec())
|
||||||
|
.isOK;
|
||||||
|
|
||||||
|
/// Upload a new custom emoji
|
||||||
|
static Future<void> uploadNewCustomEmoji(NewEmoji newEmoji) async =>
|
||||||
|
await APIRequest(
|
||||||
|
uri: "settings/upload_custom_emoji",
|
||||||
|
needLogin: true,
|
||||||
|
args: {"shortcut": newEmoji.shortcut})
|
||||||
|
.addBytesFile("image", newEmoji.image)
|
||||||
|
.execWithFilesAndThrow();
|
||||||
|
|
||||||
|
/// Delete a custom emoji
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> deleteCustomEmoji(int emojiID) async =>
|
||||||
|
(await APIRequest(uri: "settings/delete_custom_emoji", needLogin: true)
|
||||||
|
.addInt("emojiID", emojiID)
|
||||||
|
.exec())
|
||||||
|
.assertOk();
|
||||||
|
|
||||||
|
/// Check current user password
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> checkUserPassword(String password) async =>
|
||||||
|
await APIRequest(uri: "settings/check_password", needLogin: true)
|
||||||
|
.addString("password", password)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Change user password
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> changePassword(
|
||||||
|
String oldPassword, String newPassword) async =>
|
||||||
|
await APIRequest(uri: "settings/update_password", needLogin: true)
|
||||||
|
.addString("oldPassword", oldPassword)
|
||||||
|
.addString("newPassword", newPassword)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Retrieve security settings of the user
|
||||||
|
///
|
||||||
|
/// This method throws in case of failure
|
||||||
|
static Future<SecuritySettings> getSecuritySettings(String password) async {
|
||||||
|
final response =
|
||||||
|
(await APIRequest(uri: "settings/get_security", needLogin: true)
|
||||||
|
.addString("password", password)
|
||||||
|
.execWithThrow())
|
||||||
|
.getObject();
|
||||||
|
|
||||||
|
return SecuritySettings(
|
||||||
|
securityQuestion1: response["security_question_1"],
|
||||||
|
securityAnswer1: response["security_answer_1"],
|
||||||
|
securityQuestion2: response["security_question_2"],
|
||||||
|
securityAnswer2: response["security_answer_2"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply new security settings to the user
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> setSecuritySettings(
|
||||||
|
String password, SecuritySettings newSettings) async {
|
||||||
|
await APIRequest(uri: "settings/set_security", needLogin: true)
|
||||||
|
.addString("password", password)
|
||||||
|
.addString("security_question_1", newSettings.securityQuestion1)
|
||||||
|
.addString("security_answer_1", newSettings.securityAnswer1)
|
||||||
|
.addString("security_question_2", newSettings.securityQuestion2)
|
||||||
|
.addString("security_answer_2", newSettings.securityAnswer2)
|
||||||
|
.execWithThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get account data conservation policy settings
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<DataConservationPolicySettings>
|
||||||
|
getDataConservationPolicy() async {
|
||||||
|
final response =
|
||||||
|
(await APIRequest.withLogin("settings/get_data_conservation_policy")
|
||||||
|
.execWithThrow())
|
||||||
|
.getObject();
|
||||||
|
|
||||||
|
return DataConservationPolicySettings(
|
||||||
|
inactiveAccountLifeTime: response["inactive_account_lifetime"],
|
||||||
|
notificationLifetime: response["notification_lifetime"],
|
||||||
|
commentsLifetime: response["comments_lifetime"],
|
||||||
|
postsLifetime: response["posts_lifetime"],
|
||||||
|
conversationMessagesLifetime:
|
||||||
|
response["conversation_messages_lifetime"],
|
||||||
|
likesLifetime: response["likes_lifetime"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply new data conservation policy settings
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> setDataConservationPolicy(
|
||||||
|
String password, DataConservationPolicySettings newSettings) async {
|
||||||
|
await APIRequest(
|
||||||
|
uri: "settings/set_data_conservation_policy", needLogin: true)
|
||||||
|
.addString("password", password)
|
||||||
|
.addInt("inactive_account_lifetime",
|
||||||
|
newSettings.inactiveAccountLifeTime ?? 0)
|
||||||
|
.addInt("notification_lifetime", newSettings.notificationLifetime ?? 0)
|
||||||
|
.addInt("comments_lifetime", newSettings.commentsLifetime ?? 0)
|
||||||
|
.addInt("posts_lifetime", newSettings.postsLifetime ?? 0)
|
||||||
|
.addInt("conversation_messages_lifetime",
|
||||||
|
newSettings.conversationMessagesLifetime ?? 0)
|
||||||
|
.addInt("likes_lifetime", newSettings.likesLifetime ?? 0)
|
||||||
|
.execWithThrow();
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,13 @@ import 'package:meta/meta.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class SurveyHelper {
|
class SurveyHelper {
|
||||||
|
/// Get information about a single survey
|
||||||
|
static Future<Survey> getSurveyInfo(int postID) async =>
|
||||||
|
apiToSurvey((await APIRequest.withLogin("surveys/get_info")
|
||||||
|
.addInt("postID", postID)
|
||||||
|
.execWithThrow())
|
||||||
|
.getObject());
|
||||||
|
|
||||||
/// Cancel the response of a user to a survey
|
/// Cancel the response of a user to a survey
|
||||||
Future<bool> cancelResponse(Survey survey) async {
|
Future<bool> cancelResponse(Survey survey) async {
|
||||||
return (await APIRequest(
|
return (await APIRequest(
|
||||||
@ -34,6 +41,23 @@ class SurveyHelper {
|
|||||||
.isOK;
|
.isOK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new choice in a survey
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> createNewChoice(int postID, String newChoice) async =>
|
||||||
|
await APIRequest.withLogin("surveys/create_new_choice")
|
||||||
|
.addInt("postID", postID)
|
||||||
|
.addString("choice", newChoice)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Prevent new choices from being created on a survey
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> blockNewChoicesCreation(int postID) async =>
|
||||||
|
await APIRequest.withLogin("surveys/block_new_choices_creation")
|
||||||
|
.addInt("postID", postID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
/// Turn an API entry into a [Survey] object
|
/// Turn an API entry into a [Survey] object
|
||||||
static Survey apiToSurvey(Map<String, dynamic> map) {
|
static Survey apiToSurvey(Map<String, dynamic> map) {
|
||||||
// Parse survey responses
|
// Parse survey responses
|
||||||
@ -50,6 +74,7 @@ class SurveyHelper {
|
|||||||
question: map["question"],
|
question: map["question"],
|
||||||
userChoice: map["user_choice"],
|
userChoice: map["user_choice"],
|
||||||
choices: choices,
|
choices: choices,
|
||||||
|
allowNewChoicesCreation: map["allowNewChoices"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:comunic/enums/user_page_visibility.dart';
|
import 'package:comunic/enums/user_page_visibility.dart';
|
||||||
import 'package:comunic/helpers/database/users_database_helper.dart';
|
import 'package:comunic/helpers/database/users_database_helper.dart';
|
||||||
|
import 'package:comunic/lists/custom_emojies_list.dart';
|
||||||
import 'package:comunic/lists/users_list.dart';
|
import 'package:comunic/lists/users_list.dart';
|
||||||
import 'package:comunic/models/advanced_user_info.dart';
|
import 'package:comunic/models/advanced_user_info.dart';
|
||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/custom_emoji.dart';
|
||||||
import 'package:comunic/models/user.dart';
|
import 'package:comunic/models/user.dart';
|
||||||
|
|
||||||
/// User helper
|
/// User helper
|
||||||
@ -44,21 +46,21 @@ class UsersHelper {
|
|||||||
final list = UsersList();
|
final list = UsersList();
|
||||||
response.getObject().forEach(
|
response.getObject().forEach(
|
||||||
(k, v) => list.add(
|
(k, v) => list.add(
|
||||||
User(
|
User(
|
||||||
id: v["userID"],
|
id: v["userID"],
|
||||||
firstName: v["firstName"],
|
firstName: v["firstName"],
|
||||||
lastName: v["lastName"],
|
lastName: v["lastName"],
|
||||||
pageVisibility: v["publicPage"] == "false"
|
pageVisibility: v["publicPage"] == "false"
|
||||||
|
? UserPageVisibility.PRIVATE
|
||||||
|
: (v["openPage"] == "false"
|
||||||
? UserPageVisibility.PRIVATE
|
? UserPageVisibility.PRIVATE
|
||||||
: (v["openPage"] == "false"
|
: UserPageVisibility.OPEN),
|
||||||
? UserPageVisibility.PRIVATE
|
virtualDirectory:
|
||||||
: UserPageVisibility.OPEN),
|
v["virtualDirectory"] == "" ? null : v["virtualDirectory"],
|
||||||
virtualDirectory: v["virtualDirectory"] == ""
|
accountImageURL: v["accountImage"],
|
||||||
? null
|
customEmojies: _parseCustomEmojies(v["customEmojis"]),
|
||||||
: v["virtualDirectory"],
|
),
|
||||||
accountImageURL: v["accountImage"],
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save the list
|
// Save the list
|
||||||
@ -71,7 +73,8 @@ class UsersHelper {
|
|||||||
/// of failure
|
/// of failure
|
||||||
Future<UsersList> getListWithThrow(Set<int> users,
|
Future<UsersList> getListWithThrow(Set<int> users,
|
||||||
{bool forceDownload = false}) async {
|
{bool forceDownload = false}) async {
|
||||||
final list = await getUsersInfo(users.toList());
|
final list =
|
||||||
|
await getUsersInfo(users.toList(), forceDownload: forceDownload);
|
||||||
|
|
||||||
if (list == null)
|
if (list == null)
|
||||||
throw Exception(
|
throw Exception(
|
||||||
@ -81,14 +84,23 @@ class UsersHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get information about a single user. Throws in case of failure
|
/// Get information about a single user. Throws in case of failure
|
||||||
Future<User> getSingleWithThrow(int user) async {
|
Future<User> getSingleWithThrow(int user,
|
||||||
return (await getListWithThrow(Set<int>()..add(user)))[0];
|
{bool forceDownload = false}) async {
|
||||||
|
return (await getListWithThrow(Set<int>()..add(user),
|
||||||
|
forceDownload: forceDownload))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get users information from a given [Set]
|
/// Get users information from a given [Set]
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
Future<UsersList> getList(Set<int> users,
|
Future<UsersList> getList(Set<int> users,
|
||||||
{bool forceDownload = false}) async {
|
{bool forceDownload = false}) async {
|
||||||
return await getUsersInfo(users.toList());
|
final list = await getUsersInfo(users.toList());
|
||||||
|
|
||||||
|
if (list == null)
|
||||||
|
throw Exception("Failed to get the list of users!");
|
||||||
|
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get users information
|
/// Get users information
|
||||||
@ -97,7 +109,7 @@ class UsersHelper {
|
|||||||
/// the server, otherwise cached data will be used if available
|
/// the server, otherwise cached data will be used if available
|
||||||
Future<UsersList> getUsersInfo(List<int> users,
|
Future<UsersList> getUsersInfo(List<int> users,
|
||||||
{bool forceDownload = false}) async {
|
{bool forceDownload = false}) async {
|
||||||
List<int> toDownload = List();
|
List<int> toDownload = [];
|
||||||
UsersList list = UsersList();
|
UsersList list = UsersList();
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
@ -154,8 +166,30 @@ class UsersHelper {
|
|||||||
virtualDirectory:
|
virtualDirectory:
|
||||||
data["virtualDirectory"] == "" ? null : data["virtualDirectory"],
|
data["virtualDirectory"] == "" ? null : data["virtualDirectory"],
|
||||||
accountImageURL: data["accountImage"],
|
accountImageURL: data["accountImage"],
|
||||||
|
customEmojies: _parseCustomEmojies(data["customEmojis"]),
|
||||||
publicNote: data["publicNote"],
|
publicNote: data["publicNote"],
|
||||||
canPostTexts: data["can_post_texts"],
|
canPostTexts: data["can_post_texts"],
|
||||||
|
isFriendsListPublic: data["friend_list_public"],
|
||||||
|
numberFriends: data["number_friends"],
|
||||||
|
accountCreationTime: data["account_creation_time"],
|
||||||
|
personalWebsite: data["personnalWebsite"],
|
||||||
|
likes: data["pageLikes"],
|
||||||
|
userLike: data["user_like_page"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse the list of custom emojies
|
||||||
|
CustomEmojiesList _parseCustomEmojies(List<dynamic> list) {
|
||||||
|
final l = list.cast<Map<String, dynamic>>();
|
||||||
|
|
||||||
|
return CustomEmojiesList()
|
||||||
|
..addAll(l
|
||||||
|
.map((f) => CustomEmoji(
|
||||||
|
id: f["id"],
|
||||||
|
userID: f["userID"],
|
||||||
|
shortcut: f["shortcut"],
|
||||||
|
url: f["url"],
|
||||||
|
))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
20
lib/helpers/version_helper.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:package_info/package_info.dart';
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
/// Application version helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class VersionHelper {
|
||||||
|
static PackageInfo _info;
|
||||||
|
|
||||||
|
static Future<void> ensureLoaded() async {
|
||||||
|
_info = await PackageInfo.fromPlatform();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current version information
|
||||||
|
static PackageInfo get info => _info;
|
||||||
|
|
||||||
|
/// Get current application version, in parsed format
|
||||||
|
static Version get version => Version.parse(info.version);
|
||||||
|
}
|
52
lib/helpers/virtual_directory_helper.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Virtual directory helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
enum VirtualDirectoryType { USER, GROUP, NONE }
|
||||||
|
|
||||||
|
class VirtualDirectoryResult {
|
||||||
|
final VirtualDirectoryType type;
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const VirtualDirectoryResult({
|
||||||
|
@required this.type,
|
||||||
|
this.id,
|
||||||
|
}) : assert(type != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
class VirtualDirectoryHelper {
|
||||||
|
/// Find a virtual directory
|
||||||
|
Future<VirtualDirectoryResult> find(String directory) async {
|
||||||
|
final response = await APIRequest(
|
||||||
|
uri: "virtualDirectory/find",
|
||||||
|
needLogin: true,
|
||||||
|
args: {"directory": directory}).exec();
|
||||||
|
|
||||||
|
switch (response.code) {
|
||||||
|
case 404:
|
||||||
|
return VirtualDirectoryResult(type: VirtualDirectoryType.NONE);
|
||||||
|
|
||||||
|
case 200:
|
||||||
|
final id = response.getObject()["id"];
|
||||||
|
final kind = response.getObject()["kind"];
|
||||||
|
switch (kind) {
|
||||||
|
case "user":
|
||||||
|
return VirtualDirectoryResult(
|
||||||
|
type: VirtualDirectoryType.USER, id: id);
|
||||||
|
case "group":
|
||||||
|
return VirtualDirectoryResult(
|
||||||
|
type: VirtualDirectoryType.GROUP, id: id);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Exception("Unsupported virtual directory kind: $kind");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception("Could not get virtual directory!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
lib/helpers/webapp_helper.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:comunic/helpers/conversations_helper.dart';
|
||||||
|
import 'package:comunic/helpers/friends_helper.dart';
|
||||||
|
import 'package:comunic/lists/memberships_list.dart';
|
||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/membership.dart';
|
||||||
|
|
||||||
|
/// Web application helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class WebAppHelper {
|
||||||
|
/// Fetch from the server the list of memberships of the user
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<MembershipList> getMemberships() async {
|
||||||
|
final response =
|
||||||
|
(await APIRequest.withLogin("webApp/getMemberships").execWithThrow())
|
||||||
|
.getArray();
|
||||||
|
|
||||||
|
return MembershipList()
|
||||||
|
..addAll(response
|
||||||
|
.cast<Map<String, dynamic>>()
|
||||||
|
.map(_apiToMembership)
|
||||||
|
.where((f) => f != null)
|
||||||
|
..toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn an API entry into a membership entry
|
||||||
|
static Membership _apiToMembership(Map<String, dynamic> entry) {
|
||||||
|
switch (entry["type"]) {
|
||||||
|
case "conversation":
|
||||||
|
return Membership.conversation(
|
||||||
|
ConversationsHelper.apiToConversation(entry["conv"]));
|
||||||
|
|
||||||
|
case "friend":
|
||||||
|
return Membership.friend(FriendsHelper.apiToFriend(entry["friend"]));
|
||||||
|
|
||||||
|
case "group":
|
||||||
|
return Membership.group(
|
||||||
|
groupID: entry["id"], groupLastActive: entry["last_activity"]);
|
||||||
|
|
||||||
|
default:
|
||||||
|
print("Unknown membership type: ${entry["type"]}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
253
lib/helpers/websocket_helper.dart
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:comunic/helpers/comments_helper.dart';
|
||||||
|
import 'package:comunic/helpers/conversations_helper.dart';
|
||||||
|
import 'package:comunic/helpers/events_helper.dart';
|
||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/config.dart';
|
||||||
|
import 'package:comunic/models/ws_message.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
|
/// User web socket helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class WebSocketHelper {
|
||||||
|
static WebSocketChannel _ws;
|
||||||
|
|
||||||
|
static int _counter = 0;
|
||||||
|
|
||||||
|
static final _requests = Map<String, Completer<dynamic>>();
|
||||||
|
|
||||||
|
/// Check out whether we are currently connected to WebSocket or not
|
||||||
|
static bool isConnected() {
|
||||||
|
return _ws != null && _ws.closeCode == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get WebSocket access token
|
||||||
|
static Future<String> _getWsToken() async =>
|
||||||
|
(await APIRequest(uri: "ws/token", needLogin: true).exec())
|
||||||
|
.assertOk()
|
||||||
|
.getObject()["token"];
|
||||||
|
|
||||||
|
/// Connect to WebSocket
|
||||||
|
static connect() async {
|
||||||
|
if (isConnected()) return;
|
||||||
|
|
||||||
|
// First, get an access token
|
||||||
|
final token = await _getWsToken();
|
||||||
|
|
||||||
|
// Determine WebSocket URI
|
||||||
|
final wsURL =
|
||||||
|
"${(config().apiServerSecure ? "wss" : "ws")}://${config().apiServerName}${config().apiServerUri}ws?token=$token";
|
||||||
|
final wsURI = Uri.parse(wsURL);
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
_ws = WebSocketChannel.connect(wsURI);
|
||||||
|
|
||||||
|
_ws.stream.listen(
|
||||||
|
// When we got data
|
||||||
|
(data) {
|
||||||
|
print("WS New data: $data");
|
||||||
|
_processMessage(data.toString());
|
||||||
|
},
|
||||||
|
|
||||||
|
// Print errors on console
|
||||||
|
onError: (e, stack) {
|
||||||
|
print("WS error! $e");
|
||||||
|
print(stack);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Notify when the channel is closed
|
||||||
|
onDone: () {
|
||||||
|
print("WS Channel closed");
|
||||||
|
|
||||||
|
// Clear Futures queue
|
||||||
|
_requests.clear();
|
||||||
|
|
||||||
|
_ws = null;
|
||||||
|
EventsHelper.emit(WSClosedEvent());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close current WebSocket (if any)
|
||||||
|
static close() {
|
||||||
|
if (isConnected()) _ws.sink.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a new message
|
||||||
|
///
|
||||||
|
/// This method might throw an [Exception] in case of failure
|
||||||
|
static Future<dynamic> sendMessage(String title, dynamic data) {
|
||||||
|
if (!isConnected())
|
||||||
|
throw Exception("WS: Trying to send message but websocket is closed!");
|
||||||
|
|
||||||
|
final completer = Completer();
|
||||||
|
|
||||||
|
final id = "freq-${_counter++}";
|
||||||
|
|
||||||
|
final msg = WsMessage(id: id, title: title, data: data).toJSON();
|
||||||
|
|
||||||
|
print("WS Send message: $msg");
|
||||||
|
|
||||||
|
_ws.sink.add(msg);
|
||||||
|
|
||||||
|
_requests[id] = completer;
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process incoming message
|
||||||
|
static _processMessage(String msgStr) {
|
||||||
|
try {
|
||||||
|
final msg = WsMessage.fromJSON(jsonDecode(msgStr));
|
||||||
|
|
||||||
|
if (!msg.hasId)
|
||||||
|
_processUnattendedMessage(msg);
|
||||||
|
else
|
||||||
|
_respondRequests(msg);
|
||||||
|
} catch (e, stack) {
|
||||||
|
print("WS could not process message: $e");
|
||||||
|
print(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process an unattended message
|
||||||
|
static _processUnattendedMessage(WsMessage msg) {
|
||||||
|
switch (msg.title) {
|
||||||
|
// New number of notifications
|
||||||
|
case "number_notifs":
|
||||||
|
EventsHelper.emit(NewNumberNotifsEvent(msg.data));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// New number of unread conversations
|
||||||
|
case "number_unread_conversations":
|
||||||
|
EventsHelper.emit(NewNumberUnreadConversations(msg.data));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// New comment
|
||||||
|
case "new_comment":
|
||||||
|
EventsHelper.emit(
|
||||||
|
NewCommentEvent(CommentsHelper.apiToComment(msg.data)));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Updated comment
|
||||||
|
case "comment_updated":
|
||||||
|
EventsHelper.emit(
|
||||||
|
UpdatedCommentEvent(CommentsHelper.apiToComment(msg.data)));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Deleted comment
|
||||||
|
case "comment_deleted":
|
||||||
|
EventsHelper.emit(DeletedCommentEvent(msg.data));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// A user is writing a new message
|
||||||
|
case "writing_message_in_conv":
|
||||||
|
EventsHelper.emit(WritingMessageInConversationEvent(
|
||||||
|
msg.data["conv_id"], msg.data["user_id"]));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Created new conversation message
|
||||||
|
case "new_conv_message":
|
||||||
|
EventsHelper.emit(NewConversationMessageEvent(
|
||||||
|
ConversationsHelper.apiToConversationMessage(msg.data)));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Update conversation message content
|
||||||
|
case "updated_conv_message":
|
||||||
|
EventsHelper.emit(UpdatedConversationMessageEvent(
|
||||||
|
ConversationsHelper.apiToConversationMessage(msg.data)));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Deleted a conversation message
|
||||||
|
case "deleted_conv_message":
|
||||||
|
EventsHelper.emit(DeletedConversationMessageEvent(
|
||||||
|
ConversationsHelper.apiToConversationMessage(msg.data)));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Removed user from conversation
|
||||||
|
case "removed_user_from_conv":
|
||||||
|
EventsHelper.emit(RemovedUserFromConversationEvent(
|
||||||
|
msg.data["conv_id"], msg.data["user_id"]));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Conversation deleted
|
||||||
|
case "deleted_conversation":
|
||||||
|
EventsHelper.emit(DeletedConversationEvent(msg.data));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// A user joined a call
|
||||||
|
case "user_joined_call":
|
||||||
|
EventsHelper.emit(
|
||||||
|
UserJoinedCallEvent(msg.data["callID"], msg.data["userID"]));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// A user left a call
|
||||||
|
case "user_left_call":
|
||||||
|
EventsHelper.emit(
|
||||||
|
UserLeftCallEvent(msg.data["callID"], msg.data["userID"]));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Got new call signal
|
||||||
|
case "new_call_signal":
|
||||||
|
Map<String, dynamic> signalData = msg.data["data"];
|
||||||
|
EventsHelper.emit(NewCallSignalEvent(
|
||||||
|
callID: msg.data["callID"],
|
||||||
|
peerID: msg.data["peerID"],
|
||||||
|
candidate: signalData.containsKey("candidate")
|
||||||
|
? RTCIceCandidate(
|
||||||
|
signalData["candidate"],
|
||||||
|
"${signalData["sdpMLineIndex"]}" /* fix plugin crash */,
|
||||||
|
signalData["sdpMLineIndex"])
|
||||||
|
: null,
|
||||||
|
sessionDescription: signalData.containsKey("type")
|
||||||
|
? RTCSessionDescription(signalData["sdp"], signalData["type"])
|
||||||
|
: null));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Call peer ready event
|
||||||
|
case "call_peer_ready":
|
||||||
|
EventsHelper.emit(
|
||||||
|
CallPeerReadyEvent(msg.data["callID"], msg.data["peerID"]));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Call peer interrupted streaming event
|
||||||
|
case "call_peer_interrupted_streaming":
|
||||||
|
EventsHelper.emit(CallPeerInterruptedStreamingEvent(
|
||||||
|
msg.data["callID"], msg.data["peerID"]));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// The call has been closed
|
||||||
|
case "call_closed":
|
||||||
|
EventsHelper.emit(CallClosedEvent(msg.data));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Exception("Unknown message type: ${msg.title}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process responses to requests
|
||||||
|
static _respondRequests(WsMessage msg) {
|
||||||
|
if (!_requests.containsKey(msg.id))
|
||||||
|
throw Exception(
|
||||||
|
"Could not find request ${msg.id} in the requests queue!");
|
||||||
|
|
||||||
|
final completer = _requests.remove(msg.id);
|
||||||
|
|
||||||
|
// Handles errors
|
||||||
|
if (msg.title != "success") {
|
||||||
|
completer.completeError(Exception("Could not process request!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completer.complete(msg.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function
|
||||||
|
Future<dynamic> ws(String title, dynamic data) =>
|
||||||
|
WebSocketHelper.sendMessage(title, data);
|
@ -5,7 +5,7 @@ import 'dart:collection';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class AbstractList<E> extends ListBase<E> {
|
class AbstractList<E> extends ListBase<E> {
|
||||||
final _list = List<E>();
|
final _list = <E>[];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
21
lib/lists/call_members_list.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:comunic/lists/abstract_list.dart';
|
||||||
|
import 'package:comunic/models/call_member.dart';
|
||||||
|
|
||||||
|
/// Call members list
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class CallMembersList extends AbstractList<CallMember> {
|
||||||
|
/// Get the IDs of the users in this list
|
||||||
|
Set<int> get usersID => this.map((f) => f.userID).toSet();
|
||||||
|
|
||||||
|
/// Remove a specific member from this list
|
||||||
|
void removeUser(int userID) => this.removeWhere((f) => f.userID == userID);
|
||||||
|
|
||||||
|
/// Get the connection of a specific user
|
||||||
|
CallMember getUser(int userID) => this.firstWhere((f) => f.userID == userID);
|
||||||
|
|
||||||
|
/// Extract ready peers from this list
|
||||||
|
CallMembersList get readyPeers =>
|
||||||
|
CallMembersList()..addAll(where((f) => f.status == MemberStatus.READY));
|
||||||
|
}
|
@ -9,7 +9,7 @@ import 'package:comunic/models/comment.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class CommentsList extends ListBase<Comment> {
|
class CommentsList extends ListBase<Comment> {
|
||||||
List<Comment> _list = List();
|
List<Comment> _list = [];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import 'package:comunic/models/conversation_message.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class ConversationMessagesList extends ListBase<ConversationMessage> {
|
class ConversationMessagesList extends ListBase<ConversationMessage> {
|
||||||
final List<ConversationMessage> _list = List();
|
final List<ConversationMessage> _list = [];
|
||||||
|
|
||||||
set length(int v) => _list.length = v;
|
set length(int v) => _list.length = v;
|
||||||
|
|
||||||
@ -24,11 +24,10 @@ class ConversationMessagesList extends ListBase<ConversationMessage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the list of the users ID who own a message in this list
|
/// Get the list of the users ID who own a message in this list
|
||||||
List<int> getUsersID() {
|
Set<int> getUsersID() {
|
||||||
final List<int> users = List();
|
final Set<int> users = Set();
|
||||||
|
|
||||||
for (ConversationMessage message in this)
|
for (ConversationMessage message in this) users.addAll(message.usersID);
|
||||||
if (!users.contains(message.userID)) users.add(message.userID);
|
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
@ -48,4 +47,13 @@ class ConversationMessagesList extends ListBase<ConversationMessage> {
|
|||||||
if (message.id < firstMessageID) firstMessageID = message.id;
|
if (message.id < firstMessageID) firstMessageID = message.id;
|
||||||
return firstMessageID;
|
return firstMessageID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace a message by another one (if any)
|
||||||
|
void replace(ConversationMessage msg) {
|
||||||
|
final index = this.indexWhere((t) => t.id == msg.id);
|
||||||
|
if (index >= 0) this[index] = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a message from this list
|
||||||
|
void removeMsg(int id) => removeWhere((f) => f.id == id);
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,11 @@ import 'package:comunic/models/conversation.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class ConversationsList extends ListBase<Conversation> {
|
class ConversationsList extends ListBase<Conversation> {
|
||||||
|
final List<Conversation> _list = [];
|
||||||
final List<Conversation> _list = List();
|
|
||||||
UsersList users;
|
UsersList users;
|
||||||
|
|
||||||
set length(l) => _list.length = l;
|
set length(l) => _list.length = l;
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -22,12 +22,9 @@ class ConversationsList extends ListBase<Conversation> {
|
|||||||
void operator []=(int index, Conversation value) => _list[index] = value;
|
void operator []=(int index, Conversation value) => _list[index] = value;
|
||||||
|
|
||||||
/// Get the entire lists of users ID in this list
|
/// Get the entire lists of users ID in this list
|
||||||
List<int> get allUsersID {
|
Set<int> get allUsersID {
|
||||||
final List<int> list = List();
|
final Set<int> list = Set();
|
||||||
forEach((c) => c.members.forEach((id){
|
forEach((c) => c.members.forEach((member) => list.add(member.userID)));
|
||||||
if(!list.contains(id))
|
|
||||||
list.add(id);
|
|
||||||
}));
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
lib/lists/custom_emojies_list.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:comunic/lists/abstract_list.dart';
|
||||||
|
import 'package:comunic/models/custom_emoji.dart';
|
||||||
|
|
||||||
|
/// Custom emojies list
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
class CustomEmojiesList extends AbstractList<CustomEmoji> {
|
||||||
|
/// Check if an emoji, identified by its shortcut, is present in this list
|
||||||
|
bool hasShortcut(String shortcut) =>
|
||||||
|
firstWhere((f) => f.shortcut == shortcut, orElse: () => null) != null;
|
||||||
|
|
||||||
|
/// Serialize this list
|
||||||
|
List<Map<String, dynamic>> toSerializableList() =>
|
||||||
|
map((f) => f.toMap()).toList();
|
||||||
|
|
||||||
|
/// Un-serialize this list
|
||||||
|
static CustomEmojiesList fromSerializedList(List<dynamic> list) =>
|
||||||
|
CustomEmojiesList()
|
||||||
|
..addAll(list.map((f) => CustomEmoji.fromMap(f)).toList());
|
||||||
|
}
|
@ -7,7 +7,7 @@ import 'package:comunic/models/friend.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class FriendsList extends ListBase<Friend> {
|
class FriendsList extends ListBase<Friend> {
|
||||||
List<Friend> _list = List();
|
List<Friend> _list = [];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
11
lib/lists/group_members_list.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:comunic/lists/abstract_list.dart';
|
||||||
|
import 'package:comunic/models/group_membership.dart';
|
||||||
|
|
||||||
|
/// Group members list
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class GroupMembersList extends AbstractList<GroupMembership> {
|
||||||
|
/// Get the list of users in this set
|
||||||
|
Set<int> get usersID => map((f) => f.userID).toSet();
|
||||||
|
}
|
@ -7,7 +7,6 @@ import 'package:comunic/models/group.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class GroupsList extends MapBase<int, Group> {
|
class GroupsList extends MapBase<int, Group> {
|
||||||
|
|
||||||
final Map<int, Group> _groups = Map();
|
final Map<int, Group> _groups = Map();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -25,5 +24,5 @@ class GroupsList extends MapBase<int, Group> {
|
|||||||
@override
|
@override
|
||||||
Group remove(Object key) => _groups.remove(key);
|
Group remove(Object key) => _groups.remove(key);
|
||||||
|
|
||||||
|
Group getGroup(int id) => this[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
lib/lists/memberships_list.dart
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import 'package:comunic/lists/abstract_list.dart';
|
||||||
|
import 'package:comunic/models/membership.dart';
|
||||||
|
|
||||||
|
/// Memberships list
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class MembershipList extends AbstractList<Membership> {
|
||||||
|
/// Get the IDs of all the users included in some way in this list
|
||||||
|
Set<int> get usersId {
|
||||||
|
final s = Set<int>();
|
||||||
|
|
||||||
|
forEach((m) {
|
||||||
|
switch (m.type) {
|
||||||
|
case MembershipType.FRIEND:
|
||||||
|
s.add(m.friend.id);
|
||||||
|
break;
|
||||||
|
case MembershipType.GROUP:
|
||||||
|
break;
|
||||||
|
case MembershipType.CONVERSATION:
|
||||||
|
s.addAll(m.conversation.membersID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the ID of the groups included in this list
|
||||||
|
Set<int> get groupsId => where((f) => f.type == MembershipType.GROUP)
|
||||||
|
.map((f) => f.groupID)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
/// Remove a friend membership from the list
|
||||||
|
void removeFriend(int friendID) => remove(firstWhere(
|
||||||
|
(f) => f.type == MembershipType.FRIEND && f.friend.id == friendID));
|
||||||
|
}
|
@ -9,7 +9,7 @@ import 'package:comunic/models/post.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class PostsList extends ListBase<Post> {
|
class PostsList extends ListBase<Post> {
|
||||||
List<Post> _list = List();
|
List<Post> _list = [];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
20
lib/lists/search_results_list.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:comunic/lists/abstract_list.dart';
|
||||||
|
import 'package:comunic/models/search_result.dart';
|
||||||
|
|
||||||
|
/// List of result of a global search on the database
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class SearchResultsList extends AbstractList<SearchResult> {
|
||||||
|
/// Get the list of users included in this search result
|
||||||
|
Set<int> get usersId => this
|
||||||
|
.where((f) => f.kind == SearchResultKind.USER)
|
||||||
|
.map((f) => f.id)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
/// Get the list of groups included in this search result
|
||||||
|
Set<int> get groupsId => this
|
||||||
|
.where((f) => f.kind == SearchResultKind.GROUP)
|
||||||
|
.map((f) => f.id)
|
||||||
|
.toSet();
|
||||||
|
}
|
15
lib/lists/unread_conversations_list.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'package:comunic/lists/abstract_list.dart';
|
||||||
|
import 'package:comunic/models/unread_conversation.dart';
|
||||||
|
|
||||||
|
/// List of unread conversations
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class UnreadConversationsList extends AbstractList<UnreadConversation> {
|
||||||
|
/// Get the ID of the users included in this list
|
||||||
|
Set<int> get usersID {
|
||||||
|
final set = Set<int>();
|
||||||
|
forEach((element) => set.addAll(element.message.usersID));
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ import 'package:comunic/models/user.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class UsersList extends ListBase<User> {
|
class UsersList extends ListBase<User> {
|
||||||
List<User> _list = List();
|
List<User> _list = [];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
@ -31,6 +31,9 @@ class UsersList extends ListBase<User> {
|
|||||||
throw "User not found in the list!";
|
throw "User not found in the list!";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the user is included in this list or not
|
||||||
|
bool hasUser(int userID) => any((f) => f.id == userID);
|
||||||
|
|
||||||
/// Get the list of users ID present in this list
|
/// Get the list of users ID present in this list
|
||||||
List<int> get usersID => List.generate(length, (i) => this[i].id);
|
List<int> get usersID => List.generate(length, (i) => this[i].id);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:comunic/helpers/account_helper.dart';
|
import 'package:comunic/helpers/account_helper.dart';
|
||||||
import 'package:comunic/helpers/database/database_helper.dart';
|
import 'package:comunic/helpers/database/database_helper.dart';
|
||||||
import 'package:comunic/helpers/preferences_helper.dart';
|
import 'package:comunic/helpers/preferences_helper.dart';
|
||||||
import 'package:comunic/ui/routes/home_route.dart';
|
import 'package:comunic/helpers/version_helper.dart';
|
||||||
import 'package:comunic/ui/routes/login_route.dart';
|
import 'package:comunic/ui/widgets/init_widget.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Main file of the application
|
/// Main file of the application
|
||||||
@ -11,58 +11,52 @@ import 'package:flutter/material.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
void subMain() async {
|
void subMain() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Load package information
|
||||||
|
await VersionHelper.ensureLoaded();
|
||||||
|
|
||||||
// Connect to database
|
// Connect to database
|
||||||
await DatabaseHelper.open();
|
await DatabaseHelper.open();
|
||||||
|
await DatabaseHelper.cleanUpDatabase();
|
||||||
|
|
||||||
|
// Get current system language
|
||||||
|
await initTranslations();
|
||||||
|
|
||||||
|
// Check if the user is currently signed in
|
||||||
|
await AccountHelper().signedIn();
|
||||||
|
|
||||||
runApp(ComunicApplication(
|
runApp(ComunicApplication(
|
||||||
darkMode: (await PreferencesHelper.getInstance())
|
preferences: await PreferencesHelper.getInstance(),
|
||||||
.getBool(PreferencesKeyList.ENABLE_DARK_THEME),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComunicApplication extends StatelessWidget {
|
class ComunicApplication extends StatefulWidget {
|
||||||
final bool darkMode;
|
final PreferencesHelper preferences;
|
||||||
|
|
||||||
const ComunicApplication({Key key, @required this.darkMode})
|
const ComunicApplication({
|
||||||
: assert(darkMode != null),
|
Key key,
|
||||||
|
@required this.preferences,
|
||||||
|
}) : assert(preferences != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ComunicApplicationState createState() => ComunicApplicationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComunicApplicationState extends State<ComunicApplication> {
|
||||||
|
/// Use this method to force the application to rebuild
|
||||||
|
void refresh() => setState(() {});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final prefs = widget.preferences.preferences;
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: ComunicApplicationHome(),
|
home: InitializeWidget(),
|
||||||
theme: darkMode ? ThemeData.dark() : ThemeData.light(),
|
theme: prefs.enableDarkMode ? ThemeData.dark() : null,
|
||||||
|
showPerformanceOverlay: prefs.showPerformancesOverlay,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComunicApplicationHome extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _ComunicApplicationHomeState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ComunicApplicationHomeState extends State<ComunicApplicationHome> {
|
|
||||||
bool _signedIn;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
AccountHelper().signedIn().then((v) {
|
|
||||||
setState(() {
|
|
||||||
_signedIn = v;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (_signedIn == null) return buildLoadingPage();
|
|
||||||
|
|
||||||
if (_signedIn)
|
|
||||||
return HomeRoute();
|
|
||||||
else
|
|
||||||
return LoginRoute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:comunic/main.dart';
|
import 'package:comunic/main.dart';
|
||||||
import 'package:comunic/models/config.dart';
|
import 'package:comunic/models/config.dart';
|
||||||
|
|
||||||
@ -5,13 +7,26 @@ import 'package:comunic/models/config.dart';
|
|||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
/// Fix HTTPS issue
|
||||||
|
class MyHttpOverride extends HttpOverrides {
|
||||||
|
@override
|
||||||
|
HttpClient createHttpClient(SecurityContext context) {
|
||||||
|
return super.createHttpClient(context)
|
||||||
|
..badCertificateCallback = (cert, host, port) {
|
||||||
|
return host == "devweb.local"; // Forcefully trust local website
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Config.set(Config(
|
Config.set(Config(
|
||||||
apiServerName: "devweb.local",
|
apiServerName: "192.168.1.9:3000",
|
||||||
apiServerUri: "/comunic/api/",
|
apiServerUri: "/",
|
||||||
apiServerSecure: false,
|
apiServerSecure: false,
|
||||||
serviceName: "ComunicFlutter",
|
clientName: "ComunicFlutter",
|
||||||
serviceToken: "G9sZCBmb3IgVWJ1bnR1CkNvbW1lbnRbbmVdPeCkieCkrOCkq"));
|
));
|
||||||
|
|
||||||
|
HttpOverrides.global = new MyHttpOverride();
|
||||||
|
|
||||||
subMain();
|
subMain();
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,11 @@ import 'package:comunic/models/config.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Config.set(Config(
|
Config.set(Config(
|
||||||
apiServerName: "api.communiquons.org",
|
apiServerName: "api.communiquons.org",
|
||||||
apiServerUri: "/",
|
apiServerUri: "/",
|
||||||
apiServerSecure: true,
|
apiServerSecure: true,
|
||||||
serviceName: "ComunicFlutter",
|
clientName: "ComunicFlutter",
|
||||||
serviceToken: "9KfSwmB76U9UUwjXngDG7PeYccNfy"));
|
));
|
||||||
|
|
||||||
subMain();
|
subMain();
|
||||||
}
|
}
|
||||||
|
21
lib/models/account_image_settings.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// Account image settings
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
enum AccountImageVisibilityLevels { EVERYONE, COMUNIC_USERS, FRIENDS_ONLY }
|
||||||
|
|
||||||
|
class AccountImageSettings {
|
||||||
|
final bool hasImage;
|
||||||
|
final String imageURL;
|
||||||
|
final AccountImageVisibilityLevels visibility;
|
||||||
|
|
||||||
|
const AccountImageSettings({
|
||||||
|
@required this.hasImage,
|
||||||
|
@required this.imageURL,
|
||||||
|
@required this.visibility,
|
||||||
|
}) : assert(hasImage != null),
|
||||||
|
assert(imageURL != null),
|
||||||
|
assert(visibility != null);
|
||||||
|
}
|