1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2025-07-01 06:03:29 +00:00

497 Commits
1.0.1 ... 1.1.2

Author SHA1 Message Date
06ab90de1e Fix typo 2021-03-13 19:08:23 +01:00
6e4207f517 Fix translation issue 2021-03-13 19:02:08 +01:00
bc0dcbdbb1 Use cleartext traffic in beta 2021-03-13 18:37:39 +01:00
a994d9978c Add missing translations 2021-03-13 18:31:28 +01:00
30b3dc6921 Fix bad translation settings 2021-03-13 18:28:56 +01:00
b1b7772532 Add new french translations 2021-03-13 18:23:12 +01:00
f8910c8f8b Force square account image 2021-03-13 18:11:28 +01:00
e70aaabbc9 Simplify image picking code 2021-03-13 18:03:20 +01:00
ea45bf828c Can crop image 2021-03-13 17:50:59 +01:00
2a00530126 Finish to fix deprecation warnings 2021-03-13 15:42:19 +01:00
ad2cf6d4f9 Continue to fix deprecation warnings 2021-03-13 15:38:43 +01:00
230cb2c018 Continue to fix deprecation warnings 2021-03-13 15:28:34 +01:00
819e2a7590 Start Flutter update 2021-03-13 15:14:54 +01:00
fb80f3bd52 Handle conversation deleted event 2021-03-13 13:52:18 +01:00
f9db9aa632 Remove misplaced try catch block 2021-03-13 13:32:38 +01:00
a630a5ae79 Handle user removed from conversation events 2021-03-13 12:16:57 +01:00
ed9f5e396c Delete conversation => leave conversation 2021-03-13 12:07:16 +01:00
4614f3ae2e Add call notice on conversations list 2021-03-13 12:02:24 +01:00
d10b1d0d22 Fix issue with large screens 2021-03-13 11:48:33 +01:00
99ae726c0a Can remove conversation logo 2021-03-13 11:42:58 +01:00
642f5e11fc Can change conversation image 2021-03-13 11:33:25 +01:00
cbbda7237b Fix invalid check 2021-03-13 11:09:54 +01:00
0b2f939376 Can update conversation settings 2021-03-13 11:08:08 +01:00
fdec22c28a Can update admin status of a user 2021-03-13 11:02:44 +01:00
bd73e265cc Can add members to conversation 2021-03-13 10:52:53 +01:00
5d0ead5889 Can remove members from conversation 2021-03-13 10:48:59 +01:00
ba60fa9e37 Can create conversations 2021-03-13 10:32:11 +01:00
f54cc22fc6 Can change conversation color 2021-03-13 10:09:17 +01:00
d8b2dd2599 Show who is writing messages 2021-03-13 09:29:54 +01:00
7ccc7a492e Fix icon color 2021-03-13 09:10:55 +01:00
1e0e2fca52 Send writing messages event 2021-03-13 09:09:26 +01:00
dbb2a3f1a1 Can cancel file sending 2021-03-13 09:00:00 +01:00
bd5ed8fb33 Change send messages color 2021-03-13 08:41:54 +01:00
3546bacc83 Fix issues on white mode 2021-03-13 08:40:37 +01:00
201200299c Improve messages appearance 2021-03-13 08:37:59 +01:00
c1196a6359 Show files thumbnails, if available 2021-03-13 08:33:55 +01:00
bedc3f5277 Add user account image 2021-03-13 08:26:56 +01:00
5a25769b71 Start to improve messages appearance 2021-03-13 08:17:54 +01:00
05c806b358 Fix colors 2021-03-12 23:09:40 +01:00
70eb088756 Remove useless code 2021-03-12 23:07:26 +01:00
b0cfeec513 Apply conversation color 2021-03-12 22:45:55 +01:00
e35a0d2fd4 Improve send message form 2021-03-12 22:40:33 +01:00
e638398b2e Work progress on conversation form 2021-03-12 22:18:32 +01:00
f3626f233f Remove intrusive permission 2021-03-12 21:39:24 +01:00
ece9164d93 Fix audio record 2021-03-12 21:19:40 +01:00
e7b1beca50 Start to record MP3 files 2021-03-12 20:52:26 +01:00
6fc1a263d2 Enforce thumbnail width for videos 2021-03-12 19:37:25 +01:00
b84eba59e3 Generate video thumbnails 2021-03-12 19:36:42 +01:00
8f7ca14586 Can take videos 2021-03-12 19:10:10 +01:00
19d4e1d31c Extend the possibilities of file picker 2021-03-12 18:54:15 +01:00
701d5d3c27 File update system 2021-03-12 17:47:09 +01:00
ec4ca238de Improve update message dialog 2021-03-12 16:37:21 +01:00
f70717a987 Hide useless menu options 2021-03-12 16:21:45 +01:00
e02ab259b6 Updated conversations policy 2021-03-12 16:19:07 +01:00
a4181e3d42 Remove un-used dependency 2021-03-11 20:35:22 +01:00
858f81d05e Add a video player 2021-03-11 20:31:06 +01:00
46affd4e68 Avoid too big dialog 2021-03-11 20:20:41 +01:00
3518594eea Integrate an audio player in the application 2021-03-11 20:15:49 +01:00
8f2574a555 Fix unread conversations drop down 2021-03-11 18:50:36 +01:00
3257fd865f Fix conversations appeareance in sidebar 2021-03-11 18:18:58 +01:00
f9502d1700 Apply conversation icon to more places 2021-03-11 18:11:24 +01:00
b9babd43a8 Display icons for all conversations 2021-03-11 18:00:06 +01:00
c8ca80f6e7 Added conversation message statistics 2021-03-11 17:27:20 +01:00
217111e3fd Can copy message to clipboard 2021-03-11 17:04:18 +01:00
8705aa1b0d Resize conversation images 2021-03-11 00:25:58 +01:00
0458d5431c Fix files background color 2021-03-11 00:23:11 +01:00
75a80b1018 Fix date appearance 2021-03-11 00:15:04 +01:00
52d217a89c Simplify conversation files appearance 2021-03-11 00:13:05 +01:00
1f1ed0cda4 Fix display issue 2021-03-11 00:06:29 +01:00
6c00e0bcab Show message files 2021-03-11 00:02:41 +01:00
2989e98c50 Add support for server conversation message 2021-03-10 19:08:18 +01:00
08c77340a0 Add support for conversation image 2021-03-10 18:12:56 +01:00
a23b76b552 Parse correctly conversation color 2021-03-10 18:04:29 +01:00
dacccf57b5 Start conversation upgrade 2021-03-10 17:54:41 +01:00
b094361f5a Fix bad translation 2021-02-20 12:39:22 +01:00
1ee9a2c5cc Update build script 2021-02-20 12:32:03 +01:00
25a25e4c70 Update makefile 2021-02-20 12:12:19 +01:00
d4a0748249 Add missing french translations 2021-02-20 10:12:01 +01:00
1ea286f3ef Add download link on deprecation dialog 2021-02-20 09:59:21 +01:00
a32e968992 Terms of use are not hard coded anymore 2021-02-20 09:35:03 +01:00
0cd9371460 Show deprecation warning 2021-02-20 09:24:51 +01:00
1b0a3fd24b Add new build configuration 2021-02-20 09:03:17 +01:00
54e37b3e69 Display current build version on startup screen 2021-02-20 08:58:03 +01:00
2519adeef4 Fix typo 2021-02-20 08:41:56 +01:00
4f9001cb2b Remove useless configuration load checks 2021-02-19 17:38:12 +01:00
613ceadfaa Load configuration at application startup 2021-02-19 17:32:38 +01:00
459757b292 Fix issues with WebSocket 2021-02-18 19:36:51 +01:00
e399f71a78 Fix issue when WebSocket is closed 2021-02-18 19:11:50 +01:00
c5d1512375 Apply password policy on all forms 2021-02-18 18:58:47 +01:00
16ec9a8e00 Rename route with a more logical name 2021-02-18 18:28:57 +01:00
c19cbaac88 Fix invalid call 2021-02-18 18:27:24 +01:00
277c08048d Implement password policy for account creation 2021-02-18 18:20:50 +01:00
482e938744 Fix bad layout 2021-02-16 19:36:14 +01:00
1d0bd45632 Finish data conservation policy screen 2021-02-16 19:35:52 +01:00
3a39387365 Start to integrate data conservation policy 2021-02-16 19:30:06 +01:00
4d885affb9 Add Makefile 2021-02-13 17:13:09 +01:00
0ad8d5c393 Fix bad sign out order 2021-02-13 16:07:23 +01:00
e5ed4fadda Upgrade login system 2021-02-13 16:03:07 +01:00
581059cb1d Deprecate old movie system 2021-02-13 11:27:13 +01:00
0c526abfe8 Start to work on next version 2021-02-13 08:45:29 +01:00
f3ccb4e89a Add missing translations 2021-02-12 22:49:29 +01:00
24765789df Improve account image settings 2021-02-12 22:45:17 +01:00
52cc8407d5 Fix bugs in application settings 2021-02-12 22:36:22 +01:00
523f9fd0e3 Add beta splash screen 2021-02-12 19:19:40 +01:00
9ce1a401d9 Add main splash screen 2021-02-12 19:17:08 +01:00
a07c6cd415 Change app icon 2021-02-12 19:01:24 +01:00
a3e5b1a70b Rationalize config 2021-02-12 18:20:28 +01:00
5e51fbe617 Attempt to fix call issue 2021-02-07 18:44:57 +01:00
7c82aa5edf Downgrade Gradle 2021-02-07 17:53:53 +01:00
0db7de01ee Make project compile again 2021-02-07 17:32:29 +01:00
ee6e28426c Start update 2021-02-07 17:09:08 +01:00
df915e1ca3 Update dependencies 2021-02-07 16:05:10 +01:00
e33cdf56a9 Update Flutter 2021-02-07 15:58:42 +01:00
bbcc3c9d02 Merge branch 'master' of http://192.168.1.10:3000/Pierre/FLUTTER-Comunic 2021-02-06 13:28:33 +01:00
6db9eade90 Update code 2021-02-06 13:26:35 +01:00
c759b54024 Update translations 2021-02-04 17:10:47 +01:00
138d70a6e1 Update 2021-02-04 16:55:41 +01:00
4e2974a2e2 Add new french translations 2021-02-04 16:48:22 +01:00
e7073292b5 Add new translations 2021-02-04 16:08:27 +01:00
06aa84e3a1 Update dependencies 2020-06-09 18:31:51 +02:00
ac2736dd23 Correction de petites erreurs 2020-05-28 07:26:23 +02:00
9c53572f94 Tristan : debut de la traduction en allemand 2020-05-28 07:25:25 +02:00
b337e632bd Can reload survey 2020-05-18 19:15:16 +02:00
e0d69ab504 Can create a new choice for the survey 2020-05-18 18:43:57 +02:00
5086f45cc1 Fix bad check 2020-05-18 18:24:04 +02:00
acc81acdea Can block the creation of new responses on a survey 2020-05-18 18:19:07 +02:00
b4465cc70c Allow to create new choices on survey on its creation 2020-05-18 17:58:54 +02:00
ef0ae2a586 Do not offer to open private conversation on user page 2020-05-17 15:02:29 +02:00
9697ba8149 Made left pane scrollable if required 2020-05-17 14:57:34 +02:00
d70c2751bb Add a padding to the page 2020-05-17 14:56:21 +02:00
cac23dd620 Display user website 2020-05-17 14:53:26 +02:00
3794eca68b Display user note 2020-05-17 14:26:52 +02:00
e3c11e48b2 Show user membership 2020-05-17 14:17:37 +02:00
cfc3552011 Show friends list as a dialog 2020-05-16 17:36:52 +02:00
f8ba06d0ae Avoid a user to appear in its own friends list 2020-05-16 17:31:16 +02:00
6528ef5bc2 Auto adapt the list of friends to show 2020-05-16 17:24:47 +02:00
bf2ba03912 Add more actions 2020-05-16 17:21:33 +02:00
e7181c1f59 Display virtual directory 2020-05-16 14:01:59 +02:00
9b0c1323b6 Start to show user information 2020-05-16 13:41:11 +02:00
61098ffc43 Start to create user page tablet mode 2020-05-16 11:04:58 +02:00
b364c7aece Prepare for tablet mode of user page 2020-05-16 10:20:27 +02:00
288cd492a2 Improve user access denied screen 2020-05-16 09:46:38 +02:00
736accaca4 Fix missing splash effect on memberships pane 2020-05-16 09:23:59 +02:00
922fce541f Put post create form into a card 2020-05-16 09:20:17 +02:00
bc3972082c Improve show more comments button 2020-05-16 09:17:49 +02:00
fbe22c5031 Create post container 2020-05-16 09:10:09 +02:00
7f1130aca8 Do not show too many comments at the same time 2020-05-15 13:34:53 +02:00
313e7a4b3c Fix issue 2020-05-13 18:38:57 +02:00
e231f26231 Handles correctly notifications 2020-05-13 18:36:20 +02:00
9a2efaae1c Can open groups page 2020-05-13 18:33:15 +02:00
67e881af0d Can toggle performances overlay 2020-05-13 18:26:21 +02:00
a6ce969e89 Can force to use mobile mode 2020-05-13 18:21:30 +02:00
d04b40f662 Add force mobile mode preference 2020-05-13 18:17:50 +02:00
21d844b2ba Add missing padding 2020-05-13 18:13:28 +02:00
4c11aa9753 Make settings route responsive 2020-05-13 18:12:45 +02:00
dfcc49eab4 Fix invalid translation 2020-05-13 17:50:09 +02:00
395b9aca6b Add access to settings from tablet app bar 2020-05-13 17:40:29 +02:00
70e3efc93d Can toggle dark mode from menu on tablet mode 2020-05-12 19:27:23 +02:00
fe3f0c0e85 Automatically apply new dark theme settings 2020-05-12 19:18:42 +02:00
6b8cc2569d Fix issue on first start 2020-05-12 19:08:08 +02:00
17f271dfb9 Change the way we determine whether current user is signed in or not 2020-05-12 19:04:53 +02:00
c747d3c1ba Create ApplicationPreferences object 2020-05-12 19:00:35 +02:00
57e504540a Refactor settings files 2020-05-12 18:42:33 +02:00
9e5e93da39 Remove old application settings route 2020-05-12 18:40:40 +02:00
f7bf69fbfd Deactivate old settings page 2020-05-12 18:40:07 +02:00
78ffc38534 Create application settings section 2020-05-12 17:50:05 +02:00
466803a1ac Add a comment 2020-05-11 18:57:37 +02:00
39f4e8c78c Can logout in tablet mode 2020-05-11 18:56:28 +02:00
4c749ec6ac Made search results work 2020-05-11 18:39:07 +02:00
620ad0d5cf Add [WillPopScope] on tablet mode 2020-05-11 18:32:58 +02:00
f3eda1c89d Can open conversation in full screen 2020-05-11 18:27:00 +02:00
a01ebe7d8a Hide back button in tablet mode 2020-05-11 18:21:09 +02:00
f4ced0324c Highlight active conversation 2020-05-11 18:12:49 +02:00
5c26626f92 Can go back to newest posts screen 2020-05-11 13:53:15 +02:00
e7f360c6fc Can easily open current user page 2020-05-11 13:51:57 +02:00
773cf17da0 Highlight active user / group in memberships panel 2020-05-11 13:50:13 +02:00
1bd2bdd1aa Fix pages navigation issue on mobile version 2020-05-11 13:40:48 +02:00
c65558ef6c Fix pages navigation issue 2020-05-11 13:38:51 +02:00
1309e7ad3a Make membership buttons live 2020-05-11 13:27:31 +02:00
096fd42f22 Start to display widgets 2020-05-11 13:24:01 +02:00
27a56ae533 Fix issue with posts list 2020-05-10 18:48:26 +02:00
e3130c9e4b Fix issue 2020-05-10 18:34:54 +02:00
260002270f Fix issue 2020-05-10 18:32:14 +02:00
a119f60fdb Simplify pages system 2020-05-10 18:29:43 +02:00
a53ae381dc Can toggle fullscreen on calls 2020-05-10 15:21:10 +02:00
6bdaf83563 Make close window button lives 2020-05-10 15:07:20 +02:00
476f08681b Use custom app bar for call windows 2020-05-10 15:00:26 +02:00
cf9f93dcb3 Improve code 2020-05-10 14:36:33 +02:00
1d7e846973 Improve buttons bar 2020-05-10 14:32:44 +02:00
ef186f79d2 Can move window 2020-05-10 14:09:44 +02:00
b40308c9e4 Start to show call window 2020-05-10 13:46:45 +02:00
63a8e61e80 Improve call membership tile 2020-05-09 20:19:16 +02:00
25222e9156 Manage to add call button in a visible way 2020-05-09 20:11:54 +02:00
642820127c Ready to implement call system 2020-05-09 19:45:07 +02:00
44f417a0f2 Improve video appearance 2020-05-09 16:06:24 +02:00
ce908d1b51 Reduce minimal messages size 2020-05-09 14:57:59 +02:00
4db9191fd8 Can open conversation from unread conversations dropdown 2020-05-09 14:56:33 +02:00
c262e40e81 Can create new conversations 2020-05-09 14:38:58 +02:00
27176531cf Can change conversation settings 2020-05-09 14:21:02 +02:00
b23aa782b8 Can call MainController from dialogs 2020-05-09 14:18:09 +02:00
4367dcc728 Use a key for main controller 2020-05-09 14:07:14 +02:00
6d95d5f7a2 Remove a useless widget 2020-05-09 13:50:18 +02:00
db52c495ec Fix theming issue 2020-05-09 12:47:00 +02:00
32aa73a951 Fix bad check 2020-05-09 12:41:15 +02:00
67c217715a Display the list of members of a conversation 2020-05-09 12:37:06 +02:00
f45bcd691e Fix button color 2020-05-09 12:31:38 +02:00
099f282cd6 Ready to start to show the list of members 2020-05-09 12:16:55 +02:00
032815b29f Visually notify user of new messages when conversation is collapsed 2020-05-09 10:39:37 +02:00
123eaad296 Add an icon to the conversations windows 2020-05-09 10:26:15 +02:00
b020895a8c Fix typo 2020-05-09 10:20:44 +02:00
2cea14a012 Can collpase conversation window 2020-05-09 10:20:06 +02:00
38f0257fa3 Remove a warning 2020-05-09 10:07:05 +02:00
150529a740 Identify conversation windows with keys 2020-05-09 09:59:05 +02:00
6cbe34379e Display conversation 2020-05-09 09:51:37 +02:00
782e6bc978 Use theme 2020-05-09 09:44:41 +02:00
e7e81da05d Fix conversation alignments 2020-05-09 09:32:10 +02:00
af8e558d9f Create window container 2020-05-09 09:30:46 +02:00
109ba3f04b Ready to build conversation windows 2020-05-09 08:17:52 +02:00
68b4c79960 Auto-hide conversations list when a conversation is selected 2020-05-09 07:28:17 +02:00
5ccd3d4884 Change the way menu appear in conversations list screen 2020-05-09 07:14:38 +02:00
6cb9fd97e2 Use small add button 2020-05-09 07:10:18 +02:00
fe17f81a40 Display the list of conversation to open one 2020-05-09 07:07:14 +02:00
b0fd0d7d51 Add button to open new conversations 2020-05-09 06:49:05 +02:00
13cd3186f5 Attempt to avoid autofocus 2020-05-08 21:07:51 +02:00
3835ec4fe1 Add padding on conversation dropdown 2020-05-08 21:05:56 +02:00
33eb0c5aed Made search live 2020-05-08 21:02:39 +02:00
af079cc4b0 Create search field 2020-05-08 16:06:10 +02:00
2311af8219 Improve dropdown position 2020-05-08 15:44:33 +02:00
a64a192d27 Update the number of unread conversations 2020-05-08 15:42:44 +02:00
5fb845732f Display the list of unread conversations 2020-05-08 15:40:13 +02:00
935d7dbb63 Improve groups page performances 2020-05-08 09:44:41 +02:00
ccb1547523 Disable BBCode parsing 2020-05-08 09:32:45 +02:00
f41983304a Improve user page performances 2020-05-08 09:23:02 +02:00
edff7868f2 Disable pre-rendering 2020-05-08 09:14:25 +02:00
c7f970ae70 Add scaffold on dropdown 2020-05-07 19:15:43 +02:00
97db56ae78 Improve notifications delete button 2020-05-07 19:13:22 +02:00
89d3b79617 Simplify code 2020-05-07 19:04:58 +02:00
8943ae8144 Hide popup menu button in Notifications list 2020-05-07 19:01:04 +02:00
de063bd797 Display the list of notifications in the dropdown 2020-05-07 18:16:33 +02:00
24e08d63f8 Expose AppBarCustomDropDownWidget state to programmatically close dropdown 2020-05-07 18:12:11 +02:00
1750d6079e Highlight active option in AppBar 2020-05-07 18:07:38 +02:00
0b1dfa460a Use constants 2020-05-07 13:50:57 +02:00
cd82fac803 Start to create custom appbar overlay 2020-05-07 13:49:40 +02:00
d8fa90fc6a Add active mode 2020-05-07 13:03:08 +02:00
fe163b3c69 Fix bad names 2020-05-06 18:54:32 +02:00
86575a1e86 Display the number of unread notifications 2020-05-06 18:52:35 +02:00
87e670a520 Add missing space 2020-05-06 18:26:56 +02:00
91be430dbb ComunicAppBar => ComunicMobileAppBar 2020-05-06 18:25:55 +02:00
f31323fe23 Automatically refresh the list of memberships 2020-05-06 18:16:57 +02:00
6961694fd2 Add calls notice 2020-05-06 18:05:02 +02:00
0103779608 Highlight unread conversations 2020-05-06 17:57:49 +02:00
5c4020d511 Fix theming issues 2020-05-06 17:53:27 +02:00
968321b12c Handles groups membership request in memberships panel 2020-05-06 17:48:02 +02:00
eabb27495c Remove a TODO 2020-05-06 13:39:14 +02:00
263849266f Can respond to friendship requests from memberships bar 2020-05-06 13:38:11 +02:00
c5c544fb34 Display the list of memberships 2020-05-05 19:33:04 +02:00
286639889b Load memberships 2020-05-05 18:49:50 +02:00
3d0bfe6c3f Add user panel 2020-05-05 18:18:09 +02:00
d3132942bc Start to draw tablet mode 2020-05-05 13:31:03 +02:00
28451eddbd Prepare Tablet mode integration 2020-05-05 13:21:37 +02:00
86c89e782c Fix issue on create account route 2020-05-04 20:11:36 +02:00
5c76f6e0a6 Fix login route theme 2020-05-04 19:28:16 +02:00
e3c9105b1c Improve login route 2020-05-04 19:05:21 +02:00
0c2f3a28d0 Fix light theme issue 2020-05-03 21:13:20 +02:00
47dbb90640 Fix light theme issue 2020-05-03 21:07:02 +02:00
e00f452b98 Move "About Comunic" dialog 2020-05-03 17:00:38 +02:00
11c25ea271 Can change password 2020-05-03 16:55:00 +02:00
44ea647624 Send answers back to the server 2020-05-03 16:21:36 +02:00
3ffb24f7c5 Improve security questions form 2020-05-03 15:48:10 +02:00
e78d526bbb Show security questions 2020-05-03 15:35:07 +02:00
91e7dd4019 Show first option 2020-05-03 14:33:26 +02:00
6e274cae11 Check email address 2020-05-03 14:22:06 +02:00
e1795bac03 Enable again BBcode parsing 2020-05-02 18:22:02 +02:00
d4a39a3527 Can create a group 2020-05-02 18:15:55 +02:00
a48e7f57a6 Hide useless fields 2020-05-02 17:36:14 +02:00
07adf8c2ca Fix rights issues 2020-05-02 17:30:55 +02:00
685565e031 Can change group membership levels 2020-05-02 17:26:03 +02:00
eb66ea407b Can remove a member from a group 2020-05-02 17:05:18 +02:00
2edaedc5f3 Can respond to a group membership request 2020-05-02 15:57:49 +02:00
3c7795837c Can invite a user to join a group 2020-05-02 15:38:02 +02:00
2ec8693e85 Move a file to a more logical location 2020-05-02 15:32:06 +02:00
804457c761 Create pick user dialog 2020-05-02 15:30:19 +02:00
1227ef283c Can cancel a group membership invitation 2020-05-02 13:59:40 +02:00
f179e7e1d5 Display members roles 2020-05-02 11:28:28 +02:00
e2bf5e73dd Display the list of members of a group 2020-05-02 11:18:03 +02:00
bff73dbb21 Ready to implement group members screen 2020-05-02 09:57:33 +02:00
df111e393a Can delete group 2020-05-02 09:45:03 +02:00
9646cb7a70 Can delete group logo 2020-05-02 09:15:02 +02:00
4cb672cb16 Can generate a random logo for the group 2020-05-02 09:05:38 +02:00
1a53a26f39 Can upload a new logo for the group 2020-05-02 08:51:34 +02:00
f450a46e99 Can change group posts creation level 2020-05-01 21:13:31 +02:00
d6f2df7002 Can change group registration level 2020-05-01 21:04:50 +02:00
dbf2ed868a Simplify code 2020-05-01 20:55:24 +02:00
2d86780f0b Can change group visibility 2020-05-01 20:52:24 +02:00
cc08ed0232 Can change group description 2020-05-01 20:30:26 +02:00
971210e7e8 Can change group URL 2020-05-01 20:19:22 +02:00
8f0f50d0e4 Fix empty fields 2020-05-01 20:15:28 +02:00
519c68b092 Can change group virtual directory 2020-05-01 20:10:25 +02:00
d889321b38 Can change group name 2020-05-01 15:39:54 +02:00
257523b526 Start to build group settings screen 2020-05-01 15:12:55 +02:00
0bb9be9a72 Fix issue when user decide to sign out 2020-05-01 11:00:30 +02:00
4c1e33a264 Do delete user account 2020-05-01 10:56:57 +02:00
e204c62ba9 Ready to implement account deletion 2020-05-01 10:52:53 +02:00
9b427b9683 Can disconnect current user from all his devices 2020-05-01 09:42:01 +02:00
02e60ad87c Pop routes when WebSocket connection is interrupted 2020-05-01 09:17:08 +02:00
20ea964337 Can change security settings 2020-05-01 09:02:15 +02:00
289bf30a40 Can change password 2020-04-30 18:19:01 +02:00
29cc8558c3 Can ask user his password 2020-04-30 13:32:22 +02:00
d64d2ece05 Ready to implement security settings 2020-04-29 17:28:47 +02:00
ae9491e198 Better handle errors 2020-04-29 17:24:33 +02:00
3cf9ee39d0 Can delete custom emoji 2020-04-29 17:22:34 +02:00
5d432a5f87 Fix typo 2020-04-29 17:03:30 +02:00
c0e2516f39 Avoid duplicate shortcut creation 2020-04-29 13:49:16 +02:00
25f72bd11c Fix issue 2020-04-29 13:44:09 +02:00
225df61aa0 Can upload new custom emojies 2020-04-29 13:42:01 +02:00
e6df696077 Display user emojies 2020-04-28 19:03:23 +02:00
032b247080 Get custom emojies from server 2020-04-28 18:47:47 +02:00
c42fe7f806 Can update virtual directory 2020-04-28 17:52:22 +02:00
75e15f9f83 Can update user public note 2020-04-27 19:21:30 +02:00
88ea7e2431 Fix issue 2020-04-27 19:08:19 +02:00
f32ee89924 Can change personal website URL 2020-04-27 19:05:36 +02:00
cd355f84cc Can update "public friend list" parameter 2020-04-27 19:01:30 +02:00
38c1474d2e Can update "allow posts from friends" setting 2020-04-27 18:58:18 +02:00
9e5decae61 Update comments on user page switch 2020-04-27 18:51:54 +02:00
cf0708cd3b Can update page visibility settings 2020-04-27 18:44:41 +02:00
991d7ec1ab Can update email settings 2020-04-27 13:37:43 +02:00
6ba89b6bd1 Can update user name 2020-04-27 13:34:25 +02:00
2109c71dd6 Better conditions 2020-04-27 13:29:11 +02:00
da38ce426f Fix typo 2020-04-27 13:27:59 +02:00
8f927e9f72 Start to update general settings 2020-04-27 13:27:37 +02:00
ca1f94531f Apply new "canEveryoneAddMembers" policy 2020-04-26 15:30:29 +02:00
24f5a3e482 Add new members at the top of the screen 2020-04-26 15:28:06 +02:00
c09670ad64 Update screen 2020-04-26 15:25:56 +02:00
ace7b44595 Add the "allowEveryoneToAddMembers" setting when creating the conversation 2020-04-26 14:29:22 +02:00
9a4b61aff3 Update the "allowEveryoneToAddMembers" settings 2020-04-26 14:20:44 +02:00
1b5169eb56 Add new conversation property 2020-04-26 14:02:57 +02:00
8bd937420e Fix overflow issue 2020-04-25 17:32:25 +02:00
e1f8ad1466 Do not render survey pie chart which have not response 2020-04-25 17:28:13 +02:00
cbc0de944e Fix typo 2020-04-25 17:17:11 +02:00
da48328c92 Can create YouTube posts 2020-04-25 17:16:33 +02:00
bb62a3a159 Create widget for fullscreen image 2020-04-25 15:36:07 +02:00
4b0b41a902 Create scaffold for full screen image 2020-04-25 14:49:49 +02:00
c65d87dacc Can show user account image in full screen 2020-04-25 14:45:43 +02:00
6b8e1f205b Forgot a coma 2020-04-25 14:40:35 +02:00
b2c4f33665 Add TOS link 2020-04-25 14:39:55 +02:00
622258802b Can create weblink posts 2020-04-25 14:38:15 +02:00
d79e132420 Simplify code 2020-04-25 12:02:37 +02:00
4e951c6a78 Can create surveys 2020-04-25 11:58:45 +02:00
8af4a61072 Fix issue 2020-04-25 10:10:52 +02:00
ee5f791638 Adapt form to small screens 2020-04-25 10:01:45 +02:00
9a991b221c Improve precision 2020-04-25 08:25:32 +02:00
ba07247ec4 Can create countdown timers 2020-04-25 08:23:52 +02:00
60c135bf01 Simplify check 2020-04-24 13:37:44 +02:00
3a9bb3d13e Can post PDFs 2020-04-24 13:35:05 +02:00
2cf017ad2d Rename plugins interfaces module 2020-04-24 12:21:09 +02:00
ae391ab691 Create new plugins interface 2020-04-24 12:19:13 +02:00
de84fb6113 Fix a racing issue 2020-04-23 18:15:24 +02:00
675d1b8588 Fix a issue on release builds 2020-04-23 17:59:11 +02:00
7768e36c62 Lock device awake during calls 2020-04-23 17:37:12 +02:00
0ebccf1075 Improve buttons appearance 2020-04-23 13:39:33 +02:00
d94b535001 Improve buttons bar 2020-04-23 13:36:30 +02:00
707577f9ac Add audio only calls support 2020-04-23 13:12:40 +02:00
a74600ce4b Can toggle menubars 2020-04-22 18:55:29 +02:00
d083728251 Can switch camera 2020-04-22 18:45:01 +02:00
565f351d1e Notify when we stop streaming 2020-04-22 18:35:19 +02:00
b9a329c8f0 Start streaming 2020-04-22 18:29:00 +02:00
a16618bb51 Fix state consistency issue 2020-04-21 18:23:34 +02:00
8609e4e169 Can interrupt local streaming 2020-04-21 18:20:24 +02:00
e5ccedd180 Can toggle local stream visibility 2020-04-21 18:14:10 +02:00
6a9e0e36c1 Create Footer button widget 2020-04-21 18:08:01 +02:00
de1dceae9b Simplify code 2020-04-21 18:04:01 +02:00
2927f72674 Can toggle local streams 2020-04-21 13:46:26 +02:00
2858c50449 Prevent potential race 2020-04-21 13:29:09 +02:00
4988c8fea8 Get user media 2020-04-21 13:26:58 +02:00
167217a5a0 Can receive remote video 2020-04-20 18:13:28 +02:00
7364fc49a8 Can send ice candidates to server 2020-04-20 17:53:31 +02:00
ee6509bb9a Close remote peer connections on exit 2020-04-20 17:47:51 +02:00
e05323c3bb Fix potential issue 2020-04-20 17:34:31 +02:00
096251eaad Can send SDP back to server 2020-04-20 17:29:36 +02:00
45d903bcf7 Create peer connection for remote streams 2020-04-20 17:24:42 +02:00
473402149a Handles new signal events 2020-04-20 16:23:33 +02:00
80f05cb008 Can request call offer 2020-04-20 15:50:01 +02:00
73c7fa8807 Add hang up button 2020-04-20 15:20:01 +02:00
c0856c5126 id => userID (improve clarity) 2020-04-20 15:02:49 +02:00
99ecc399ee Handles peer ready & peer destroyed streams events 2020-04-20 14:58:23 +02:00
0f99e807f8 Add confirmation to leave call 2020-04-20 14:41:09 +02:00
456df166b1 Handle call closed event 2020-04-20 14:32:57 +02:00
08d357fc72 Handles join / leave events 2020-04-20 14:24:35 +02:00
9154fe47e1 Display the list of members of the call 2020-04-20 14:13:03 +02:00
08c2ac32aa Get the list of members of the call 2020-04-20 14:02:32 +02:00
04693cc163 Get calls configuration 2020-04-20 13:43:17 +02:00
391d3150dd Can join & leave the call 2020-04-20 13:24:40 +02:00
da641515fa Display the name of the call 2020-04-20 13:19:49 +02:00
0a03f581d1 Ready to implement calls screen 2020-04-20 10:53:25 +02:00
f227209e9b Get call status from server 2020-04-20 10:37:59 +02:00
5eaf8d6b72 Open login page if login tokens are rejected by server 2020-04-20 09:02:54 +02:00
159e7228bf Close WebSocket when InitWidget state is disposed 2020-04-20 08:52:01 +02:00
40cf1b1ddc Close WebSocket on logout 2020-04-20 08:47:08 +02:00
cb47f0351e Add WebRTC dependency 2020-04-19 15:15:07 +02:00
909e68e7bb Handles remove message events 2020-04-19 14:29:01 +02:00
88ba2d303e Handles messages update events 2020-04-19 14:16:35 +02:00
2746623b8d Handles new messages events 2020-04-19 13:58:24 +02:00
b0be889833 Register to conversation updates 2020-04-19 13:42:47 +02:00
dab4e7bde1 Handles comment deletion events 2020-04-18 16:57:00 +02:00
6b08b62832 Register to comment udpate events 2020-04-18 16:46:55 +02:00
a60c1ed68c Handles new comments events 2020-04-18 16:35:53 +02:00
02034acbbe Register to post events 2020-04-18 16:07:56 +02:00
469e1e1f92 Use WebSocket to update likes 2020-04-18 15:24:57 +02:00
526f698bf4 Automatically change the number of unread conversations 2020-04-18 14:33:07 +02:00
a8c358fd58 Fix issue 2020-04-18 14:20:07 +02:00
0700014b3a Automatically refresh the list of notifications when required 2020-04-18 14:17:14 +02:00
1b13a90615 Use Websocket to update number of unread notifications 2020-04-18 14:14:54 +02:00
36f89a9a53 Listen to WebSocket close events 2020-04-18 13:48:21 +02:00
de3cd9c7b7 Connect to WebSocket 2020-04-17 16:04:47 +02:00
9021ca7168 Rename HomeRoute => MainRoute 2020-04-17 15:26:37 +02:00
7549a9ff22 Add initialization layer 2020-04-17 15:25:26 +02:00
6239c10579 Decode HTML characters 2020-04-17 14:19:38 +02:00
78e75cffdb Improve widget 2020-04-17 14:06:01 +02:00
2c1ae783e3 Can delete all the notifications 2020-04-17 13:50:47 +02:00
8b6e464644 Improve code 2020-04-17 13:29:00 +02:00
003eb1efc2 Diplay the number of unread notifications / conversations 2020-04-17 13:28:01 +02:00
90d58bbee3 Open notifications by default 2020-04-17 12:15:13 +02:00
025e5bdf1c Fix overflow issue 2020-04-17 12:06:37 +02:00
31dfec89ee Force refresh of current user account image when image settings panel is closed 2020-04-17 12:00:42 +02:00
9b53a3b0c9 Improve links color 2020-04-17 11:56:28 +02:00
03c4a4eae0 Automatically cleanup user information 2020-04-17 11:53:47 +02:00
d822e0edd8 Can always refresh group page 2020-04-17 10:42:37 +02:00
b326507417 Automatically reset post create form 2020-04-17 10:41:10 +02:00
414f81b32f Fix issue 2020-04-17 10:37:02 +02:00
bffdab1423 Improve search appearance 2020-04-17 10:32:22 +02:00
0494a9058d Create search page 2020-04-17 10:30:29 +02:00
ef9a2c7190 Can zoom full screen images 2020-04-17 08:53:46 +02:00
7e01947da9 Can generate a random account image 2020-04-17 08:44:57 +02:00
7bb805bffd Add no account image screen 2020-04-16 19:36:34 +02:00
ef9510e731 Can delete account image 2020-04-16 19:35:09 +02:00
6e0f6d1d79 Can change account image visibility level 2020-04-16 19:30:25 +02:00
8feea380a4 Can upload new account image 2020-04-16 19:16:44 +02:00
7071600c3f Ready to implement account settings 2020-04-16 16:33:44 +02:00
806c6fbc2b Add groups notifications support 2020-04-16 16:04:15 +02:00
90cbd7ab23 Fix issue with new API 2020-04-16 15:14:27 +02:00
0ec0f216e2 Use DisplayString for messages 2020-04-16 15:10:47 +02:00
32c491ae84 Remove Navigator references from conversations pages 2020-04-16 14:58:02 +02:00
3fa45f9744 Remove useless column 2020-04-16 14:27:55 +02:00
69f8710f31 Simplify user page 2020-04-16 14:20:24 +02:00
824be11013 Cache parsed emojies 2020-04-16 14:07:21 +02:00
6e96a554ff Little optimisation 2020-04-16 13:34:49 +02:00
3bbb09e813 Fix user access denied page 2020-04-16 13:26:04 +02:00
cecf18f5a0 Turn user page route into a screen 2020-04-16 13:13:31 +02:00
7014ded7f0 Optimize parsing 2020-04-16 12:18:39 +02:00
45d3f93192 Add references support 2020-04-16 12:06:57 +02:00
2bb75da017 Add references support 2020-04-16 12:06:01 +02:00
7de882338d Parse URLs 2020-04-16 10:53:09 +02:00
3389ca18f7 Parse emojies 2020-04-16 10:14:45 +02:00
b7be59bc6e Include BBcode parser 2020-04-16 09:53:19 +02:00
ec1732088a Add about dialog 2020-04-16 09:29:37 +02:00
302e5f22ce Can load older posts on user page 2020-04-16 09:17:10 +02:00
c7502e6a04 Can load older posts on group page 2020-04-16 09:07:23 +02:00
8881ef3af4 Fix appearance issues 2020-04-16 08:58:56 +02:00
f5914a8d34 Add group create post form 2020-04-16 08:52:26 +02:00
d6f0147339 Improve scrolling of Group Page 2020-04-16 08:43:41 +02:00
cf5b1180a9 Fix issue: when creating a comment on a list of posts where user never appeared before 2020-04-16 08:34:39 +02:00
f0a23bcb47 Display group posts 2020-04-16 08:24:34 +02:00
e777c4c991 Increase like icon size on posts 2020-04-15 19:26:51 +02:00
f8a70faf28 Add like button on group page 2020-04-15 19:23:08 +02:00
dde909457d Simplify use of like widget 2020-04-15 19:17:29 +02:00
2d26395d73 Use Like widget on Post element 2020-04-15 19:07:15 +02:00
5f70669a84 Create like widget 2020-04-15 18:58:45 +02:00
add1712b7d Can update following status of a group 2020-04-15 18:39:07 +02:00
566f205dc9 Start to build group page 2020-04-15 18:23:38 +02:00
bd33e1f9c2 Handles the case of closed registrations 2020-04-15 18:09:22 +02:00
b10163575f Start to build groups page 2020-04-15 18:06:20 +02:00
c7d8843f06 Can specify arguments for pages 2020-04-15 16:03:04 +02:00
b52748a93b Automatically refresh groups list 2020-04-15 14:34:48 +02:00
8d49a80e79 Can respond to group membership invitations 2020-04-15 14:31:45 +02:00
cd677deec0 Add an icon to the requested state 2020-04-15 14:07:50 +02:00
2c9dc5199c Can cancel membership request 2020-04-15 14:04:47 +02:00
eaeb07db69 Can cancel membership request 2020-04-15 13:56:59 +02:00
d0bafd5bd9 Can remove membership 2020-04-15 13:25:55 +02:00
4bedbc4b25 Start to display user groups 2020-04-15 12:04:19 +02:00
8300fc8ca9 Fix HTTPs issue 2020-04-15 11:13:39 +02:00
f16480092b Fix new API support 2020-03-30 22:44:08 +02:00
c0b64580f9 Switch server 2020-03-30 22:43:46 +02:00
b397fe33b7 Thanks to Tristan for French translations ! 2020-03-29 17:27:40 +02:00
8655d20234 Implement translations system 2020-03-28 17:11:11 +01:00
96a3c05497 Add new translation 2020-03-28 16:29:16 +01:00
bed6539d5d First translation 2020-03-28 16:27:37 +01:00
6e867041e8 Updated Dio 2020-03-24 15:01:34 +01:00
13394811bb First build attempt 2020-03-24 14:37:00 +01:00
a19579d4fd Target SDK 29 2020-03-24 13:17:51 +01:00
7e817106b1 Updated Flutter, Gradle & migrated to android x 2020-03-24 13:15:49 +01:00
1be499f242 Add support for PDFs 2019-11-16 09:24:20 +01:00
527927da82 Fix little issues with countdown timer. 2019-11-02 20:17:00 +01:00
bcf9a1586a Add support for countdown posts 2019-11-02 20:14:34 +01:00
c714d24dfa Fix issue on slow connections 2019-11-02 20:11:06 +01:00
a603d5bd3a Create account form is working 2019-11-02 18:54:30 +01:00
32a32224ca Can open Term Of Service from Create Account Page. 2019-11-02 18:16:16 +01:00
0162224b7f Start to work on create account form 2019-11-02 18:09:34 +01:00
277 changed files with 18659 additions and 2863 deletions

3
.gitignore vendored
View File

@ -68,3 +68,6 @@
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
.flutter-plugins-dependencies

19
Makefile Normal file
View 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

View File

@ -33,7 +33,15 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 28
compileSdkVersion 30
compileOptions {
// Required to use WebRTC
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
disable 'InvalidPackage'
@ -42,11 +50,11 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "org.communiquons.comunic"
minSdkVersion 16
targetSdkVersion 28
minSdkVersion 21
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -63,6 +71,10 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
useProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@ -77,6 +89,10 @@ android {
applicationId "org.communiquons.beta"
}
}
packagingOptions {
exclude 'META-INF/proguard/androidx-annotations.pro'
}
}
flutter {
@ -85,6 +101,6 @@ flutter {
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

3
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,3 @@
## Flutter WebRTC
-keep class com.cloudwebrtc.webrtc.** { *; }
-keep class org.webrtc.** { *; }

View File

@ -3,7 +3,9 @@
package="org.communiquons.comunic">
<application
tools:replace="android:label"
android:label="Comunic Beta" />
android:label="Comunic Beta"
android:usesCleartextTraffic="true"
tools:replace="android:label" />
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#3C3F41</color>
</resources>

View File

@ -4,4 +4,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Use clear connection in dev mode -->
<application android:usesCleartextTraffic="true" />
</manifest>

View File

@ -5,17 +5,43 @@
<!-- Internet connection is required to access to the API -->
<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
calls FlutterMain.startInitialization(this); in its onCreate method.
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
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="Comunic"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@drawable/ic_app_rounded"
android:roundIcon="@mipmap/ic_launcher_round"
tools:ignore="GoogleAppIndexingWarning">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
@ -27,13 +53,28 @@
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
android:name="io.flutter.embedding.android.NormalTheme"
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>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</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>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,13 +1,6 @@
package org.communiquons.comunic;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View 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>

View File

@ -1,12 +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 android:drawable="@android:color/white" />
<item>
<shape android:shape="rectangle">
<solid android:color="#1C227E" />
</shape>
</item>
<!-- You can insert your own image assets here -->
<!-- <item>
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
android:src="@drawable/splash_icon" />
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -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>

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1BA1E2</color>
</resources>

View File

@ -5,4 +5,9 @@
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/white</item>
</style>
</resources>

View File

@ -5,7 +5,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.android.tools.build:gradle:3.5.4'
}
}

View File

@ -1 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -1,6 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
#Tue Mar 24 12:27:30 CET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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

View File

@ -0,0 +1 @@
include ':app'

3
assets/langs.json Normal file
View File

@ -0,0 +1,3 @@
{
"fr_FR": "fr"
}

85
assets/langs/de.json Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
/// Comunic mobile constants
///
/// @author Pierre Hubert
/// Data serialisation directory
const SERIALIZATION_DIRECTORY = "serialization";

View File

@ -2,13 +2,4 @@
///
/// @author Pierre HUBERT
enum PostKind {
TEXT,
IMAGE,
WEB_LINK,
PDF,
MOVIE,
COUNTDOWN,
SURVEY,
YOUTUBE
}
enum PostKind { TEXT, IMAGE, WEB_LINK, PDF, COUNTDOWN, SURVEY, YOUTUBE }

View File

@ -1,9 +1,10 @@
import 'package:comunic/helpers/api_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/authentication_details.dart';
import 'package:comunic/models/login_tokens.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:comunic/models/new_account.dart';
import 'package:comunic/models/res_check_password_reset_token.dart';
/// Account helper
///
@ -16,9 +17,14 @@ enum AuthResult {
INVALID_CREDENTIALS
}
class AccountHelper {
static const _USER_ID_PREFERENCE_NAME = "user_id";
enum CreateAccountResult {
SUCCESS,
ERROR_TOO_MANY_REQUESTS,
ERROR_EXISTING_EMAIL,
ERROR
}
class AccountHelper {
// Current user ID
static int _currentUserID = -1;
@ -27,7 +33,7 @@ class AccountHelper {
/// Warning : This method MUST BE CALLED AT LEAST ONCE AFTER APP START !!!
Future<bool> signedIn() async {
bool signedIn =
(await PreferencesHelper.getInstance()).getLoginTokens() != null;
(await PreferencesHelper.getInstance()).getLoginToken() != null;
// Load current user ID for later use
if (signedIn && _currentUserID == -1) await _loadCurrentUserID();
@ -38,8 +44,8 @@ class AccountHelper {
/// Sign in user
Future<AuthResult> signIn(AuthenticationDetails auth) async {
final request = APIRequest(uri: "account/login");
request.addString("userMail", auth.email);
request.addString("userPassword", auth.password);
request.addString("mail", auth.email);
request.addString("password", auth.password);
final response = await APIHelper().exec(request);
@ -50,10 +56,9 @@ class AccountHelper {
return AuthResult.TOO_MANY_ATTEMPTS;
else if (response.code != 200) return AuthResult.NETWORK_ERROR;
// Save login tokens
final tokensObj = response.getObject()["tokens"];
// Save login token
await (await PreferencesHelper.getInstance())
.setLoginTokens(LoginTokens(tokensObj["token1"], tokensObj["token2"]));
.setLoginToken(response.getObject()["token"]);
// Get current user ID
final userID = await _downloadCurrentUserID();
@ -63,8 +68,8 @@ class AccountHelper {
}
// Save current user ID
final preferences = await SharedPreferences.getInstance();
await preferences.setInt(_USER_ID_PREFERENCE_NAME, userID);
final preferences = await PreferencesHelper.getInstance();
await preferences.setInt(PreferencesKeyList.USER_ID, userID);
_currentUserID = userID;
return AuthResult.SUCCESS;
@ -72,16 +77,122 @@ class AccountHelper {
/// Sign out user
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;
// 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
Future<int> _downloadCurrentUserID() async {
final response = await APIRequest(
uri: "user/getCurrentUserID",
needLogin: true,
).exec();
final response = await APIRequest.withLogin("account/id").exec();
if (response.code != 200) return null;
@ -90,13 +201,33 @@ class AccountHelper {
/// Get the ID of the currently signed in user
Future<void> _loadCurrentUserID() async {
final preferences = await SharedPreferences.getInstance();
_currentUserID = preferences.getInt(_USER_ID_PREFERENCE_NAME);
final preferences = await PreferencesHelper.getInstance();
_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
static int getCurrentUserID() {
if (_currentUserID == -1) throw "Current user ID has not been loaded yet!";
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();
}
}

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:comunic/helpers/events_helper.dart';
import 'package:comunic/helpers/preferences_helper.dart';
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/api_response.dart';
@ -18,15 +19,13 @@ class APIHelper {
Future<APIResponse> exec(APIRequest request, {bool multipart = false}) async {
try {
//Add API tokens
request.addString("serviceName", config().serviceName);
request.addString("serviceToken", config().serviceToken);
request.addString("client", config().clientName);
//Add user tokens (if required)
if (request.needLogin) {
final tokens = (await PreferencesHelper.getInstance()).getLoginTokens();
assert(tokens != null);
request.addString("userToken1", tokens.tokenOne);
request.addString("userToken2", tokens.tokenTwo);
final token = (await PreferencesHelper.getInstance()).getLoginToken();
assert(token != null);
request.addString("token", token);
}
// Determine server URL
@ -37,17 +36,38 @@ class APIHelper {
else
url = Uri.https(config().apiServerName, path);
final data = FormData.from(request.args);
final data = FormData.fromMap(request.args);
// Process files (if required)
if (multipart)
request.files.forEach(
(k, v) => data.add(k, UploadFileInfo(v, v.path.split("/").last)));
if (multipart) {
// Process filesystem files
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
final response = await Dio().post(
url.toString(),
data: data,
cancelToken: request.cancelToken,
onSendProgress: request.progressCallback,
options: Options(
receiveDataWhenStatusError: 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)
return APIResponse(response.statusCode, null);
return APIResponse(response.statusCode, response.data);
return APIResponse(response.statusCode, response.data);
} catch (e) {
} catch (e, stack) {
print(e.toString());
print("Could not execute a request!");
print(stack);
return APIResponse(-1, null);
}
}

View 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");
}
}
}

View File

@ -1,5 +1,6 @@
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/comment.dart';
import 'package:comunic/models/displayed_content.dart';
import 'package:comunic/models/new_comment.dart';
/// Comments helper
@ -16,7 +17,7 @@ class CommentsHelper {
"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();
@ -64,7 +65,7 @@ class CommentsHelper {
userID: entry["userID"],
postID: entry["postID"],
timeSent: entry["time_sent"],
content: entry["content"],
content: DisplayedString(entry["content"]),
imageURL: entry["img_url"],
likes: entry["likes"],
userLike: entry["userlike"],

View File

@ -1,15 +1,25 @@
import 'package:comunic/helpers/database/conversation_messages_database_helper.dart';
import 'package:comunic/helpers/database/conversations_database_helper.dart';
import 'package:comunic/helpers/serialization/conversation_message_serialization_helper.dart';
import 'package:comunic/helpers/serialization/conversations_serialization_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/conversations_list.dart';
import 'package:comunic/lists/unread_conversations_list.dart';
import 'package:comunic/lists/users_list.dart';
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/api_response.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/displayed_content.dart';
import 'package:comunic/models/new_conversation.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/color_utils.dart';
import 'package:comunic/utils/dart_color.dart';
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
/// Conversation helper
@ -19,124 +29,149 @@ import 'package:meta/meta.dart';
enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED }
class ConversationsHelper {
final ConversationsDatabaseHelper _conversationsDatabaseHelper =
ConversationsDatabaseHelper();
final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper =
ConversationMessagesDatabaseHelper();
static final _registeredConversations = Map<int, int>();
/// Create a new conversation
///
/// Return the ID of the newly created conversation or -1 in case of failure
Future<int> createConversation(Conversation settings) async {
final response =
await APIRequest(uri: "conversations/create", needLogin: true, args: {
"name": settings.hasName ? settings.name : "false",
"follow": settings.following ? "true" : "false",
"users": settings.members.join(",")
}).exec();
if (response.code != 200) return -1;
/// Return the ID of the newly created conversation
///
/// Throws in case of failure
static Future<int> createConversation(NewConversation settings) async {
final response = await APIRequest.withLogin("conversations/create", args: {
"name": settings.name ?? "",
"follow": settings.follow ? "true" : "false",
"users": settings.members.join(","),
"color": colorToHex(settings.color)
})
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
.execWithThrow();
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
///
/// Returns a boolean depending of the success of the operation
Future<bool> updateConversation(Conversation settings) async {
final request =
APIRequest(uri: "conversations/updateSettings", needLogin: true, args: {
"conversationID": settings.id.toString(),
"following": settings.following ? "true" : "false"
});
/// Throws in case of failure
static Future<void> updateConversation(
NewConversationsSettings settings) async {
final request = APIRequest.withLogin("conversations/updateSettings")
.addInt("conversationID", settings.convID)
.addBool("following", settings.following);
// Update all conversation settings, if possible
if (settings.isOwner) {
request.addString("name", settings.hasName ? settings.name : "false");
request.addString("members", settings.members.join(","));
}
// Update conversation settings
if (settings.isComplete)
request
.addString("name", settings.name ?? "")
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
.addString("color", colorToHex(settings.color));
final response = await request.exec();
if (response.code != 200) return false;
await request.execWithThrow();
// Delete old conversation entry from the database
await _conversationsDatabaseHelper.delete(settings.id);
// Success
return true;
await ConversationsSerializationHelper()
.removeElement((t) => t.id == settings.convID);
}
/// 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]
Future<bool> deleteConversation(int id) async {
final response = await APIRequest(
uri: "conversations/delete",
needLogin: true,
args: {
"conversationID": id.toString(),
},
).exec();
return response.code == 200;
}
Future<void> deleteConversation(int id) async =>
await APIRequest.withLogin("conversations/delete")
.addInt("conversationID", id)
.execWithThrow();
/// Download the list of conversations from the server
///
/// Throws an exception in case of failure
Future<ConversationsList> downloadList() async {
final response =
await APIRequest(uri: "conversations/getList", needLogin: true).exec();
await APIRequest.withLogin("conversations/getList").execWithThrow();
if (response.code != 200) return null;
try {
ConversationsList list = ConversationsList();
response.getArray().forEach((f) => list.add(_apiToConversation(f)));
response.getArray().forEach((f) => list.add(apiToConversation(f)));
// Update the database
await _conversationsDatabaseHelper.clearTable();
await _conversationsDatabaseHelper.insertAll(list);
await ConversationsSerializationHelper().setList(list);
return list;
} on Exception catch (e) {
print(e.toString());
return null;
}
}
/// Get the local list of conversations
Future<ConversationsList> getCachedList() async {
final list = await _conversationsDatabaseHelper.getAll();
final list = await ConversationsSerializationHelper().getList();
list.sort();
return list;
}
/// Get information about a single conversation specified by its [id]
Future<Conversation> _downloadSingle(int id) async {
try {
final response = await APIRequest(
uri: "conversations/getInfoOne",
uri: "conversations/get_single",
needLogin: true,
args: {"conversationID": id.toString()}).exec();
args: {"conversationID": id.toString()}).execWithThrow();
if (response.code != 200) return null;
final conversation = apiToConversation(response.getObject());
final conversation = _apiToConversation(response.getObject());
_conversationsDatabaseHelper.insertOrUpdate(conversation);
await ConversationsSerializationHelper()
.insertOrReplaceElement((c) => c.id == conversation.id, 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
/// 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 {
if (force || !await _conversationsDatabaseHelper.has(id))
if (force ||
!await ConversationsSerializationHelper().any((c) => c.id == id))
return await _downloadSingle(id);
else
return _conversationsDatabaseHelper.get(id);
return await ConversationsSerializationHelper().get(id);
}
/// Get the name of a [conversation]. This requires information
@ -148,9 +183,9 @@ class ConversationsHelper {
String name = "";
int count = 0;
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 ? ", " : "") +
users.getUser(conversation.members[i]).fullName;
users.getUser(conversation.members[i].userID).fullName;
count++;
}
@ -162,6 +197,8 @@ class ConversationsHelper {
/// Search and return a private conversation with a given [userID]. If such
/// conversation does not exists, it is created if [allowCreate] is set to
/// true
///
/// Throws an exception in case of failure
Future<int> getPrivate(int userID, {bool allowCreate = true}) async {
final response = await APIRequest(
uri: "conversations/getPrivate",
@ -170,68 +207,78 @@ class ConversationsHelper {
"otherUser": userID.toString(),
"allowCreate": allowCreate.toString()
},
).exec();
if (response.code != 200) return null;
).execWithThrow();
// Get and return conversation ID
try {
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
/// about the members of the conversation
///
/// Returns null in case of failure
/// Throws an exception in case of failure
static Future<String> getConversationNameAsync(
Conversation conversation) async {
if (conversation.hasName) return conversation.name;
//Get information about the members of the conversation
final members = await UsersHelper().getUsersInfo(conversation.members);
if (members == null) return null;
final members = await UsersHelper().getList(conversation.membersID);
return ConversationsHelper.getConversationName(conversation, members);
}
/// Turn an API entry into a [Conversation] object
Conversation _apiToConversation(Map<String, dynamic> map) {
static Conversation apiToConversation(Map<String, dynamic> map) {
return Conversation(
id: map["ID"],
ownerID: map["ID_owner"],
lastActive: map["last_active"],
name: map["name"] == false ? null : map["name"],
following: map["following"] == 1,
sawLastMessage: map["saw_last_message"] == 1,
members: map["members"].map<int>((f) => int.parse(f)).toList(),
);
id: map["id"],
lastActivity: map["last_activity"],
name: map["name"],
color: map["color"] == null ? null : HexColor(map["color"]),
logoURL: map["logo"],
groupID: map["group_id"],
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
///
/// Throws an exception in case of failure
Future<ConversationMessagesList> _parseConversationMessageFromServer(
int conversationID, APIResponse response) async {
if (response.code != 200) return null;
response.assertOk();
// Parse the response of the server
ConversationMessagesList list = ConversationMessagesList();
response.getArray().forEach((f) {
list.add(
_apiToConversationMessage(
conversationID: conversationID,
map: f,
),
apiToConversationMessage(f),
);
});
// Save messages in the cache
_conversationMessagesDatabaseHelper.insertOrUpdateAll(list);
await ConversationsMessagesSerializationHelper(conversationID)
.insertOrReplaceAll(list);
return list;
}
@ -240,6 +287,8 @@ class ConversationsHelper {
///
/// Set [lastMessageID] to 0 to specify that we do not have any message of the
/// conversation yet or another value else
///
/// Throws an exception in case of failure
Future<ConversationMessagesList> _downloadNewMessagesSingle(
int conversationID,
{int lastMessageID = 0}) async {
@ -250,26 +299,26 @@ class ConversationsHelper {
args: {
"conversationID": conversationID.toString(),
"last_message_id": lastMessageID.toString()
}).exec();
}).execWithThrow();
return await _parseConversationMessageFromServer(conversationID, response);
}
/// Get older messages for a given conversation from an online source
///
/// Throws in case of failure
Future<ConversationMessagesList> getOlderMessages({
@required int conversationID,
@required int oldestMessagesID,
int limit = 15,
}) async {
// Perform the request online
final response = await APIRequest(
uri: "conversations/get_older_messages",
needLogin: true,
args: {
final response =
await APIRequest.withLogin("conversations/get_older_messages", args: {
"conversationID": conversationID.toString(),
"oldest_message_id": oldestMessagesID.toString(),
"limit": limit.toString()
}).exec();
}).execWithThrow();
return await _parseConversationMessageFromServer(conversationID, response);
}
@ -279,6 +328,8 @@ class ConversationsHelper {
/// If [lastMessageID] is set to 0 then we retrieve the last messages of
/// the conversation.
/// Otherwise [lastMessageID] contains the ID of the last known message
///
/// Throws in case of failure
Future<ConversationMessagesList> getNewMessages(
{@required int conversationID,
int lastMessageID = 0,
@ -287,35 +338,32 @@ class ConversationsHelper {
return await _downloadNewMessagesSingle(conversationID,
lastMessageID: lastMessageID);
else
return await _conversationMessagesDatabaseHelper
.getAllMessagesConversations(conversationID,
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);
return await ConversationsMessagesSerializationHelper(conversationID)
.getList();
}
/// Send a new message to the server
Future<SendMessageResult> sendMessage(NewConversationMessage message) async {
final request = APIRequest(
uri: "conversations/sendMessage",
needLogin: true,
args: {
"conversationID": message.conversationID.toString(),
"message": message.hasMessage ? message.message : ""
},
);
Future<SendMessageResult> sendMessage(
NewConversationMessage message, {
ProgressCallback sendProgress,
CancelToken cancelToken,
}) async {
final request = APIRequest.withLogin("conversations/sendMessage")
.addInt("conversationID", message.conversationID)
.addString("message", message.hasMessage ? message.message : "");
//Check for image
if (message.hasImage) request.addFile("image", message.image);
request.progressCallback = sendProgress;
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
APIResponse response;
if (!message.hasImage)
if (!message.hasFile)
response = await request.exec();
else
response = await request.execWithFiles();
@ -327,6 +375,15 @@ class ConversationsHelper {
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
Future<bool> updateMessage(int id, String newContent) async {
final response = await APIRequest(
@ -336,9 +393,7 @@ class ConversationsHelper {
if (response.code != 200) return false;
// Update the message content locally
return await _conversationMessagesDatabaseHelper.updateMessageContent(
id: id, newContent: newContent);
return true;
}
/// Delete permanently a message specified by its [id]
@ -351,22 +406,111 @@ class ConversationsHelper {
if (response.code != 200) return false;
// Delete the message locally
return await _conversationMessagesDatabaseHelper.delete(id);
return true;
}
/// 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
ConversationMessage _apiToConversationMessage({
@required int conversationID,
@required Map<String, dynamic> map,
}) {
return ConversationMessage(
id: map["ID"],
conversationID: conversationID,
userID: map["ID_user"],
timeInsert: map["time_insert"],
message: map["message"],
imageURL: map["image_path"],
static ConversationMessage apiToConversationMessage(
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(
id: map["id"],
convID: map["conv_id"],
userID: map["user_id"],
timeSent: map["time_sent"],
message: DisplayedString(map["message"] ?? ""),
file: file,
serverMessage: serverMessage);
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -4,7 +4,7 @@
/// Main information
class DatabaseContract {
static const DATABASE_VERSION = 1;
static const DATABASE_VERSION = 2;
static const DATABASE_FILE_NAME = "database.sqlite";
}
@ -22,29 +22,7 @@ abstract class UserTableContract {
static const C_VISIBILITY = "visibility";
static const C_VIRTUAL_DIRECTORY = "virtual_directory";
static const C_ACCOUNT_IMAGE_URL = "account_image_url";
}
/// 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";
static const C_CUSTOM_EMOJIES = "custom_emojies";
}
/// Friends table contract

View File

@ -1,4 +1,5 @@
import 'package:comunic/helpers/database/database_contract.dart';
import 'package:connectivity/connectivity.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
@ -29,6 +30,13 @@ abstract class DatabaseHelper {
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
///
/// Currently : delete all the database tables and initialize it again
@ -37,17 +45,9 @@ abstract class DatabaseHelper {
// Drop users table
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
await db.execute(
"DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}");
await db
.execute("DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}");
// Initialize database again
await _initializeDatabase(db, newVersion);
@ -62,29 +62,8 @@ abstract class DatabaseHelper {
"${UserTableContract.C_LAST_NAME} TEXT, "
"${UserTableContract.C_VISIBILITY} TEXT, "
"${UserTableContract.C_VIRTUAL_DIRECTORY} TEXT, "
"${UserTableContract.C_ACCOUNT_IMAGE_URL} 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"
"${UserTableContract.C_ACCOUNT_IMAGE_URL} TEXT, "
"${UserTableContract.C_CUSTOM_EMOJIES} TEXT"
")");
// Friends list table

View 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);
}
}

View File

@ -28,18 +28,12 @@ class FriendsHelper {
if (response.code != 200) return null;
// Parse and return the list of friends
FriendsList list = FriendsList();
response.getArray().forEach(
(f) => list.add(
Friend(
id: f["ID_friend"],
accepted: f["accepted"] == 1,
lastActive: f["time_last_activity"],
following: f["following"] == 1,
canPostTexts: f["canPostTexts"],
),
),
);
FriendsList list = FriendsList()
..addAll(response
.getArray()
.cast<Map<String, dynamic>>()
.map(apiToFriend)
.toList());
// Save the list of friends
_friendsDatabaseHelper.clearTable();
@ -48,6 +42,17 @@ class FriendsHelper {
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
Future<FriendsList> getList({@required bool online}) async {
if (online)

View File

@ -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/models/advanced_group_info.dart';
import 'package:comunic/models/api_request.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
///
@ -34,6 +41,17 @@ const _APIGroupsPostsCreationLevelsMap = {
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 {
/// Download a list of groups information from the server
Future<GroupsList> _downloadList(Set<int> groups) async {
@ -72,7 +90,7 @@ class GroupsHelper {
// Check which groups information to download
final toDownload = Set<int>();
groups.forEach((groupID) {
if (_groupsListCache.containsKey(groupID))
if (!force && _groupsListCache.containsKey(groupID))
list[groupID] = _groupsListCache[groupID];
else
toDownload.add(groupID);
@ -91,18 +109,268 @@ class GroupsHelper {
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
Group _getGroupFromAPI(Map<String, dynamic> map) {
return Group(
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: map["virtual_directory"],
virtualDirectory: nullToEmpty(map["virtual_directory"]),
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"]],
);
}

View File

@ -1,5 +1,5 @@
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';
/// Likes helper
@ -15,21 +15,15 @@ const LikesAPIMap = {
class LikesHelper {
/// Update liking status of an element
Future<bool> setLiking({
Future<void> setLiking({
@required LikesType type,
@required bool like,
@required int id,
}) async {
return (await APIRequest(
uri: "likes/update",
needLogin: true,
args: {
return (await ws("likes/update", {
"type": LikesAPIMap[type],
"like": like.toString(),
"id": id.toString(),
},
).exec())
.code ==
200;
}));
}
}

View File

@ -1,5 +1,6 @@
import 'package:comunic/lists/notifications_list.dart';
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/count_unread_notifications.dart';
import 'package:comunic/models/notification.dart';
/// Notifications helper
@ -15,7 +16,6 @@ const _NotificationElementTypeAPImapping = {
"post_text": NotificationElementType.POST_TEXT,
"post_img": NotificationElementType.POST_IMAGE,
"post_youtube": NotificationElementType.POST_YOUTUBE,
"post_movie": NotificationElementType.POST_MOVIE,
"post_weblink": NotificationElementType.POST_WEBLINK,
"post_pdf": NotificationElementType.POST_PDF,
"post_timer": NotificationElementType.POST_TIMER,
@ -47,6 +47,22 @@ const _NotificationsTypeAPImapping = {
};
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
Future<NotificationsList> getUnread() async {
final response =
@ -85,4 +101,10 @@ class NotificationsHelper {
},
).exec())
.isOK;
/// Delete all unread notifications
Future<bool> deleteAllNotifications() async =>
(await APIRequest(uri: "notifications/delete_all", needLogin: true)
.exec())
.isOK;
}

View File

@ -4,11 +4,14 @@ import 'package:comunic/enums/post_visibility_level.dart';
import 'package:comunic/enums/user_access_levels.dart';
import 'package:comunic/helpers/comments_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/posts_list.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/post.dart';
import 'package:http_parser/http_parser.dart';
/// Posts helper
///
@ -26,7 +29,6 @@ const _APIPostsKindsMap = {
"image": PostKind.IMAGE,
"weblink": PostKind.WEB_LINK,
"pdf": PostKind.PDF,
"movie": PostKind.MOVIE,
"countdown": PostKind.COUNTDOWN,
"survey": PostKind.SURVEY,
"youtube": PostKind.YOUTUBE
@ -45,6 +47,12 @@ const _APIPostsTargetKindsMap = {
};
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
/// failure
Future<PostsList> getLatest({int from = 0}) async {
@ -67,10 +75,27 @@ class PostsHelper {
/// Get the list of posts of a user
Future<PostsList> getUserPosts(int userID, {int from = 0}) async {
final response = await APIRequest(
uri: "posts/get_user",
needLogin: true,
args: {"userID": userID.toString(), "startFrom": from.toString()})
final response = await (APIRequest(uri: "posts/get_user", needLogin: true)
..addInt("userID", userID)
..addInt("startFrom", from == 0 ? 0 : from - 1))
.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();
if (response.code != 200) return null;
@ -117,7 +142,33 @@ class PostsHelper {
break;
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;
default:
@ -167,6 +218,28 @@ class PostsHelper {
.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
Post _apiToPost(Map<String, dynamic> map) {
final postKind = _APIPostsKindsMap[map["kind"]];
@ -189,7 +262,7 @@ class PostsHelper {
userPageID: map["user_page_id"],
groupID: map["group_id"],
timeSent: map["post_time"],
content: map["content"],
content: DisplayedString(map["content"]),
visibilityLevel: _APIPostsVisibilityLevelMap[map["visibility_level"]],
kind: postKind,
fileSize: map["file_size"],

View File

@ -1,6 +1,4 @@
import 'dart:convert';
import 'package:comunic/models/login_tokens.dart';
import 'package:comunic/models/application_preferences.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Preferences helper
@ -9,11 +7,20 @@ import 'package:shared_preferences/shared_preferences.dart';
///
/// @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 = {
PreferencesKeyList.LOGIN_TOKENS: "login_tokens",
PreferencesKeyList.USER_ID: "user_id",
PreferencesKeyList.LOGIN_TOKEN: "login_token",
PreferencesKeyList.ENABLE_DARK_THEME: "dark_theme",
PreferencesKeyList.FORCE_MOBILE_MODE: "force_mobile_mode",
PreferencesKeyList.SHOW_PERFORMANCE_OVERLAY: "perfs_overlay",
};
class PreferencesHelper {
@ -37,23 +44,28 @@ class PreferencesHelper {
}
/// Set new login tokens
Future<void> setLoginTokens(LoginTokens tokens) async {
await setString(PreferencesKeyList.LOGIN_TOKENS,
tokens == null ? "null" : tokens.toString());
Future<void> setLoginToken(String token) async {
if (token != null)
await setString(PreferencesKeyList.LOGIN_TOKEN, token);
else
await removeKey(PreferencesKeyList.LOGIN_TOKEN);
}
/// Get current [LoginTokens]. Returns null if none or in case of failure
LoginTokens getLoginTokens() {
String getLoginToken() {
try {
final string = getString(PreferencesKeyList.LOGIN_TOKENS);
if (string == null || string == "null") return null;
return LoginTokens.fromJSON(jsonDecode(string));
final string = getString(PreferencesKeyList.LOGIN_TOKEN);
return string;
} on Exception catch (e) {
print(e.toString());
return null;
}
}
Future<bool> removeKey(PreferencesKeyList key) async {
return await _sharedPreferences.remove(_PreferenceKeysName[key]);
}
Future<bool> setString(PreferencesKeyList key, String value) async {
return await _sharedPreferences.setString(_PreferenceKeysName[key], value);
}
@ -66,10 +78,31 @@ class PreferencesHelper {
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}) {
final v = _sharedPreferences.getBool(_PreferenceKeysName[key]);
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() {

View File

@ -1,31 +1,47 @@
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/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
///
/// @author Pierre HUBERT
class SearchHelper {
/// Search for user. This method returns information about the target users
///
/// Returns information about the target users or null if an error occurred
Future<UsersList> searchUser(String query) async {
// Execute the query on the server
final response = await APIRequest(
uri: "user/search",
needLogin: true,
args: {
"query": query
}
).exec();
uri: "user/search", needLogin: true, args: {"query": query}).exec();
if (response.code != 200) return null;
return await UsersHelper().getUsersInfo(
listToIntList(response.getArray()));
return await UsersHelper()
.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());
}
}

View 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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View 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;

View 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();
}
}

View File

@ -8,6 +8,13 @@ import 'package:meta/meta.dart';
/// @author Pierre HUBERT
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
Future<bool> cancelResponse(Survey survey) async {
return (await APIRequest(
@ -34,6 +41,23 @@ class SurveyHelper {
.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
static Survey apiToSurvey(Map<String, dynamic> map) {
// Parse survey responses
@ -50,6 +74,7 @@ class SurveyHelper {
question: map["question"],
userChoice: map["user_choice"],
choices: choices,
allowNewChoicesCreation: map["allowNewChoices"],
);
}
}

View File

@ -1,8 +1,10 @@
import 'package:comunic/enums/user_page_visibility.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/models/advanced_user_info.dart';
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/custom_emoji.dart';
import 'package:comunic/models/user.dart';
/// User helper
@ -53,10 +55,10 @@ class UsersHelper {
: (v["openPage"] == "false"
? UserPageVisibility.PRIVATE
: UserPageVisibility.OPEN),
virtualDirectory: v["virtualDirectory"] == ""
? null
: v["virtualDirectory"],
virtualDirectory:
v["virtualDirectory"] == "" ? null : v["virtualDirectory"],
accountImageURL: v["accountImage"],
customEmojies: _parseCustomEmojies(v["customEmojis"]),
),
),
);
@ -71,7 +73,8 @@ class UsersHelper {
/// of failure
Future<UsersList> getListWithThrow(Set<int> users,
{bool forceDownload = false}) async {
final list = await getUsersInfo(users.toList());
final list =
await getUsersInfo(users.toList(), forceDownload: forceDownload);
if (list == null)
throw Exception(
@ -81,14 +84,23 @@ class UsersHelper {
}
/// Get information about a single user. Throws in case of failure
Future<User> getSingleWithThrow(int user) async {
return (await getListWithThrow(Set<int>()..add(user)))[0];
Future<User> getSingleWithThrow(int user,
{bool forceDownload = false}) async {
return (await getListWithThrow(Set<int>()..add(user),
forceDownload: forceDownload))[0];
}
/// Get users information from a given [Set]
///
/// Throws in case of failure
Future<UsersList> getList(Set<int> users,
{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
@ -97,7 +109,7 @@ class UsersHelper {
/// the server, otherwise cached data will be used if available
Future<UsersList> getUsersInfo(List<int> users,
{bool forceDownload = false}) async {
List<int> toDownload = List();
List<int> toDownload = [];
UsersList list = UsersList();
// Check cache
@ -154,8 +166,30 @@ class UsersHelper {
virtualDirectory:
data["virtualDirectory"] == "" ? null : data["virtualDirectory"],
accountImageURL: data["accountImage"],
customEmojies: _parseCustomEmojies(data["customEmojis"]),
publicNote: data["publicNote"],
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());
}
}

View 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);
}

View 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!");
}
}
}

View 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;
}
}
}

View 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);

View File

@ -5,7 +5,7 @@ import 'dart:collection';
/// @author Pierre HUBERT
class AbstractList<E> extends ListBase<E> {
final _list = List<E>();
final _list = <E>[];
int get length => _list.length;

View 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));
}

View File

@ -9,7 +9,7 @@ import 'package:comunic/models/comment.dart';
/// @author Pierre HUBERT
class CommentsList extends ListBase<Comment> {
List<Comment> _list = List();
List<Comment> _list = [];
int get length => _list.length;

View File

@ -7,7 +7,7 @@ import 'package:comunic/models/conversation_message.dart';
/// @author Pierre HUBERT
class ConversationMessagesList extends ListBase<ConversationMessage> {
final List<ConversationMessage> _list = List();
final List<ConversationMessage> _list = [];
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
List<int> getUsersID() {
final List<int> users = List();
Set<int> getUsersID() {
final Set<int> users = Set();
for (ConversationMessage message in this)
if (!users.contains(message.userID)) users.add(message.userID);
for (ConversationMessage message in this) users.addAll(message.usersID);
return users;
}
@ -48,4 +47,13 @@ class ConversationMessagesList extends ListBase<ConversationMessage> {
if (message.id < firstMessageID) firstMessageID = message.id;
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);
}

View File

@ -8,11 +8,11 @@ import 'package:comunic/models/conversation.dart';
/// @author Pierre HUBERT
class ConversationsList extends ListBase<Conversation> {
final List<Conversation> _list = List();
final List<Conversation> _list = [];
UsersList users;
set length(l) => _list.length = l;
int get length => _list.length;
@override
@ -22,12 +22,9 @@ class ConversationsList extends ListBase<Conversation> {
void operator []=(int index, Conversation value) => _list[index] = value;
/// Get the entire lists of users ID in this list
List<int> get allUsersID {
final List<int> list = List();
forEach((c) => c.members.forEach((id){
if(!list.contains(id))
list.add(id);
}));
Set<int> get allUsersID {
final Set<int> list = Set();
forEach((c) => c.members.forEach((member) => list.add(member.userID)));
return list;
}
}

View 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());
}

View File

@ -7,7 +7,7 @@ import 'package:comunic/models/friend.dart';
/// @author Pierre HUBERT
class FriendsList extends ListBase<Friend> {
List<Friend> _list = List();
List<Friend> _list = [];
int get length => _list.length;

View 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();
}

View File

@ -7,7 +7,6 @@ import 'package:comunic/models/group.dart';
/// @author Pierre HUBERT
class GroupsList extends MapBase<int, Group> {
final Map<int, Group> _groups = Map();
@override
@ -25,5 +24,5 @@ class GroupsList extends MapBase<int, Group> {
@override
Group remove(Object key) => _groups.remove(key);
Group getGroup(int id) => this[id];
}

View 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));
}

View File

@ -9,7 +9,7 @@ import 'package:comunic/models/post.dart';
/// @author Pierre HUBERT
class PostsList extends ListBase<Post> {
List<Post> _list = List();
List<Post> _list = [];
int get length => _list.length;

View 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();
}

View 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;
}
}

View File

@ -7,7 +7,7 @@ import 'package:comunic/models/user.dart';
/// @author Pierre HUBERT
class UsersList extends ListBase<User> {
List<User> _list = List();
List<User> _list = [];
int get length => _list.length;
@ -31,6 +31,9 @@ class UsersList extends ListBase<User> {
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
List<int> get usersID => List.generate(length, (i) => this[i].id);
}

View File

@ -1,9 +1,9 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/helpers/database/database_helper.dart';
import 'package:comunic/helpers/preferences_helper.dart';
import 'package:comunic/ui/routes/home_route.dart';
import 'package:comunic/ui/routes/login_route.dart';
import 'package:comunic/utils/ui_utils.dart';
import 'package:comunic/helpers/version_helper.dart';
import 'package:comunic/ui/widgets/init_widget.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:flutter/material.dart';
/// Main file of the application
@ -11,58 +11,52 @@ import 'package:flutter/material.dart';
/// @author Pierre HUBERT
void subMain() async {
WidgetsFlutterBinding.ensureInitialized();
// Load package information
await VersionHelper.ensureLoaded();
// Connect to database
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(
darkMode: (await PreferencesHelper.getInstance())
.getBool(PreferencesKeyList.ENABLE_DARK_THEME),
preferences: await PreferencesHelper.getInstance(),
));
}
class ComunicApplication extends StatelessWidget {
final bool darkMode;
class ComunicApplication extends StatefulWidget {
final PreferencesHelper preferences;
const ComunicApplication({Key key, @required this.darkMode})
: assert(darkMode != null),
const ComunicApplication({
Key key,
@required this.preferences,
}) : assert(preferences != null),
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
Widget build(BuildContext context) {
final prefs = widget.preferences.preferences;
return MaterialApp(
debugShowCheckedModeBanner: false,
home: ComunicApplicationHome(),
theme: darkMode ? ThemeData.dark() : ThemeData.light(),
home: InitializeWidget(),
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();
}
}

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:comunic/main.dart';
import 'package:comunic/models/config.dart';
@ -5,13 +7,26 @@ import 'package:comunic/models/config.dart';
///
/// @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() {
Config.set(Config(
apiServerName: "devweb.local",
apiServerUri: "/comunic/api/",
apiServerName: "192.168.1.9:3000",
apiServerUri: "/",
apiServerSecure: false,
serviceName: "ComunicFlutter",
serviceToken: "G9sZCBmb3IgVWJ1bnR1CkNvbW1lbnRbbmVdPeCkieCkrOCkq"));
clientName: "ComunicFlutter",
));
HttpOverrides.global = new MyHttpOverride();
subMain();
}

View File

@ -10,8 +10,8 @@ void main() {
apiServerName: "api.communiquons.org",
apiServerUri: "/",
apiServerSecure: true,
serviceName: "ComunicFlutter",
serviceToken: "9KfSwmB76U9UUwjXngDG7PeYccNfy"));
clientName: "ComunicFlutter",
));
subMain();
}

View 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);
}

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