mirror of
https://gitlab.com/comunic/comunicmobile
synced 2025-07-01 14:13:29 +00:00
Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
06ab90de1e | |||
6e4207f517 | |||
bc0dcbdbb1 | |||
a994d9978c | |||
30b3dc6921 | |||
b1b7772532 | |||
f8910c8f8b | |||
e70aaabbc9 | |||
ea45bf828c | |||
2a00530126 | |||
ad2cf6d4f9 | |||
230cb2c018 | |||
819e2a7590 | |||
fb80f3bd52 | |||
f9db9aa632 | |||
a630a5ae79 | |||
ed9f5e396c | |||
4614f3ae2e | |||
d10b1d0d22 | |||
99ae726c0a | |||
642f5e11fc | |||
cbbda7237b | |||
0b2f939376 | |||
fdec22c28a | |||
bd73e265cc | |||
5d0ead5889 | |||
ba60fa9e37 | |||
f54cc22fc6 | |||
d8b2dd2599 | |||
7ccc7a492e | |||
1e0e2fca52 | |||
dbb2a3f1a1 | |||
bd5ed8fb33 | |||
3546bacc83 | |||
201200299c | |||
c1196a6359 | |||
bedc3f5277 | |||
5a25769b71 | |||
05c806b358 | |||
70eb088756 | |||
b0cfeec513 | |||
e35a0d2fd4 | |||
e638398b2e | |||
f3626f233f | |||
ece9164d93 | |||
e7b1beca50 | |||
6fc1a263d2 | |||
b84eba59e3 | |||
8f7ca14586 | |||
19d4e1d31c | |||
701d5d3c27 | |||
ec4ca238de | |||
f70717a987 | |||
e02ab259b6 | |||
a4181e3d42 | |||
858f81d05e | |||
46affd4e68 | |||
3518594eea | |||
8f2574a555 | |||
3257fd865f | |||
f9502d1700 | |||
b9babd43a8 | |||
c8ca80f6e7 | |||
217111e3fd | |||
8705aa1b0d | |||
0458d5431c | |||
75a80b1018 | |||
52d217a89c | |||
1f1ed0cda4 | |||
6c00e0bcab | |||
2989e98c50 | |||
08c77340a0 | |||
a23b76b552 | |||
dacccf57b5 | |||
b094361f5a |
3
Makefile
3
Makefile
@ -1,6 +1,9 @@
|
|||||||
beta_online_release:
|
beta_online_release:
|
||||||
flutter build apk --flavor beta -t lib/main_online.dart
|
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:
|
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
|
flutter build apk --flavor beta -t lib/main_online.dart --target-platform android-arm,android-arm64,android-x64 --split-per-abi
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
package="org.communiquons.comunic">
|
package="org.communiquons.comunic">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
tools:replace="android:label"
|
android:label="Comunic Beta"
|
||||||
android:label="Comunic Beta" />
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:replace="android:label" />
|
||||||
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@ -4,4 +4,7 @@
|
|||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
|
<!-- Use clear connection in dev mode -->
|
||||||
|
<application android:usesCleartextTraffic="true" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -14,6 +14,16 @@
|
|||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
|
||||||
|
<!-- This is required on Android 11+ for image picker -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.VIDEO_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
@ -26,6 +36,8 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
@ -58,5 +70,11 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- This let the image cropper work -->
|
||||||
|
<activity
|
||||||
|
android:name="com.yalantis.ucrop.UCropActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"%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% Days %hours% Hours %minutes% Minutes %seconds% Seconds": "\"%days% Jours %hours% Heures %minutes% Minutes %seconds% Secondes\"",
|
||||||
"%days%d": "%days% j",
|
"%days%d": "%days% j",
|
||||||
"%hours% h": "%hours% h",
|
"%hours% h": "%hours% h",
|
||||||
@ -9,6 +16,7 @@
|
|||||||
"%num% members": "%num% membres",
|
"%num% members": "%num% membres",
|
||||||
"%secs%s": "%secs% s",
|
"%secs%s": "%secs% s",
|
||||||
"%years% years": "%years% ans",
|
"%years% years": "%years% ans",
|
||||||
|
"'%1%' copied to clipboard!": "'%1%' copié dans le presse papier !",
|
||||||
"1 Like": "1 personne aime",
|
"1 Like": "1 personne aime",
|
||||||
"1 member": "1 membre",
|
"1 member": "1 membre",
|
||||||
"1 month": "1 mois",
|
"1 month": "1 mois",
|
||||||
@ -38,6 +46,7 @@
|
|||||||
"Add image": "Ajouter une image",
|
"Add image": "Ajouter une image",
|
||||||
"Add member": "Ajouter un membre",
|
"Add member": "Ajouter un membre",
|
||||||
"Add new emoji": "Ajouter un nouvel émoticon",
|
"Add new emoji": "Ajouter un nouvel émoticon",
|
||||||
|
"Admin": "Admin",
|
||||||
"Administrator": "Administrateur",
|
"Administrator": "Administrateur",
|
||||||
"All members": "Tous les membres",
|
"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",
|
"All the members of the group can create posts on the group": "Tous les membres du groupe peuvent créer des posts",
|
||||||
@ -59,6 +68,8 @@
|
|||||||
"Appearance": "Apparence",
|
"Appearance": "Apparence",
|
||||||
"Application settings": "Paramètres de l'application",
|
"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 !",
|
"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 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 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 comments after": "Supprimer automatiquement vos commentaires après",
|
||||||
@ -66,6 +77,7 @@
|
|||||||
"Automatically delete your likes after": "Supprimer automatiquement vos \"J'aime\" 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",
|
"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",
|
"Block the creation of new responses": "Bloquer la création de nouvelles réponses",
|
||||||
|
"Browse files": "Parcourir les fichiers",
|
||||||
"Camera": "Caméra",
|
"Camera": "Caméra",
|
||||||
"Can access to all group posts": "Peut accéder à tous les posts du groupe",
|
"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 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",
|
||||||
@ -75,14 +87,17 @@
|
|||||||
"Cancel response to survey": "Annuler la réponse au sondage",
|
"Cancel response to survey": "Annuler la réponse au sondage",
|
||||||
"Change account image visibility": "Changer la visibilité de l'image de compte",
|
"Change account image visibility": "Changer la visibilité de l'image de compte",
|
||||||
"Change level": "Changer le niveau",
|
"Change level": "Changer le niveau",
|
||||||
|
"Change logo": "Changer",
|
||||||
"Change password": "Changer le mot de passe",
|
"Change password": "Changer le mot de passe",
|
||||||
"Change your password": "Changer de mot de passe",
|
"Change your password": "Changer de mot de passe",
|
||||||
"Change your security questions": "Changer les questions de sécurité",
|
"Change your security questions": "Changer les questions de sécurité",
|
||||||
"Checking availability...": "Vérification de la disponibilité...",
|
"Checking availability...": "Vérification de la disponibilité...",
|
||||||
"Choose a new password": "Choisir un nouveau mot de passe",
|
"Choose a new password": "Choisir un nouveau mot de passe",
|
||||||
"Choose a user": "Choisir un utilisateur",
|
"Choose a user": "Choisir un utilisateur",
|
||||||
|
"Choose a video": "Choisir une vidéo",
|
||||||
"Choose a virtual directory": "Choisir un répertoire virtuel",
|
"Choose a virtual directory": "Choisir un répertoire virtuel",
|
||||||
"Choose an image": "Choisir une image",
|
"Choose an image": "Choisir une image",
|
||||||
|
"Close": "Fermer",
|
||||||
"Closed registration": "Inscription fermée",
|
"Closed registration": "Inscription fermée",
|
||||||
"Comunic": "Comunic",
|
"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 is a free and OpenSource social network that respect your privacy.": "Comunic est un réseau social libre qui respecte votre vie privée.",
|
||||||
@ -95,9 +110,14 @@
|
|||||||
"Confirm your 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é !",
|
"Congratulations! Your password has now been successfully changed!": "Félicitations ! Votre mot de passe a bien été changé !",
|
||||||
"Connected users": "Utilisateurs connectés",
|
"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 members": "Membres de la conversation",
|
||||||
|
"Conversation name (optional)": "Nom de la conversation (optionnel)",
|
||||||
"Conversation name (optionnal)": "Nom de la conversation (optionnel)",
|
"Conversation name (optionnal)": "Nom de la conversation (optionnel)",
|
||||||
"Conversations": "Conversations",
|
"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 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 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 membership request!": "Erreur lors de l'annulation de votre demande à rejoindre ce groupe !",
|
||||||
@ -137,6 +157,7 @@
|
|||||||
"Could not get user information!": "Impossible de récupérer les informations de l'utilisateur !",
|
"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 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 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 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 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 general settings!": "Erreur lors du chargement des paramètres généraux !",
|
||||||
@ -190,6 +211,9 @@
|
|||||||
"Create a new post...": "Créer un nouveau post...",
|
"Create a new post...": "Créer un nouveau post...",
|
||||||
"Create an account": "Créer un compte",
|
"Create an account": "Créer un compte",
|
||||||
"Create the conversation": "Créer la conversation",
|
"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 account image": "Image de compte actuelle",
|
||||||
"Current choices:": "Choix actuels :",
|
"Current choices:": "Choix actuels :",
|
||||||
"Current level: %level%": "Niveau actuel : %level%",
|
"Current level: %level%": "Niveau actuel : %level%",
|
||||||
@ -208,6 +232,7 @@
|
|||||||
"Delete logo": "Supprimer le logo",
|
"Delete logo": "Supprimer le logo",
|
||||||
"Delete your account": "Supprimer votre compte",
|
"Delete your account": "Supprimer votre compte",
|
||||||
"Deprecated application version": "Version obsolète de l'application",
|
"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": "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é.",
|
"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 choices creation?": "Voulez-vous vraiment bloquer la création de nouveaux choix ?",
|
||||||
@ -219,30 +244,56 @@
|
|||||||
"Do you really want to delete this custom emoji ?": "Voulez-vous vraiment supprimer cet émoticon personnalisé ?",
|
"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 ? 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 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 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 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 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 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 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 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 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 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 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 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 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 ?",
|
"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",
|
"Download update outside Play Store": "Télécharger la mise hors du Play Store",
|
||||||
"Email address": "Adresse e-mail",
|
"Email address": "Adresse e-mail",
|
||||||
"Email address...": "Adresse mail...",
|
"Email address...": "Adresse mail...",
|
||||||
"Enable dark theme": "Activer le thème sombre",
|
"Enable dark theme": "Activer le thème sombre",
|
||||||
"Error": "Erreur",
|
"Error": "Erreur",
|
||||||
"Error while creating your account": "Une erreur s'est produite lors de la création de votre compte.",
|
"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 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": "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 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",
|
"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 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 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",
|
"First name": "Prénom",
|
||||||
"Follow": "Suivre",
|
"Follow": "Suivre",
|
||||||
"Follow conversation": "Suivre la conversation",
|
"Follow conversation": "Suivre la conversation",
|
||||||
@ -291,6 +342,7 @@
|
|||||||
"Invited": "Invité",
|
"Invited": "Invité",
|
||||||
"Last name": "Nom",
|
"Last name": "Nom",
|
||||||
"Learn more about us": "En savoir plus sur nous",
|
"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.",
|
"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",
|
"Like": "J'aime",
|
||||||
"Loading": "Chargement",
|
"Loading": "Chargement",
|
||||||
@ -306,7 +358,10 @@
|
|||||||
"Members": "Membres",
|
"Members": "Membres",
|
||||||
"Membership": "Inscription",
|
"Membership": "Inscription",
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
|
"Message not seen yet": "Message non vu",
|
||||||
"Message rejected by the server!": "Message rejeté par le serveur !",
|
"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",
|
"Moderated registration": "Inscription modérée",
|
||||||
"Moderator": "Modérateur",
|
"Moderator": "Modérateur",
|
||||||
"Moderators only": "Modérateurs uniquement",
|
"Moderators only": "Modérateurs uniquement",
|
||||||
@ -321,8 +376,10 @@
|
|||||||
"New comment...": "Nouveau commentaire...",
|
"New comment...": "Nouveau commentaire...",
|
||||||
"New content...": "Nouveau contenu...",
|
"New content...": "Nouveau contenu...",
|
||||||
"New content:": "Nouveau contenu :",
|
"New content:": "Nouveau contenu :",
|
||||||
|
"New file": "Nouveau fichier",
|
||||||
"New membership level": "Nouveau niveau d'appartenance au groupe",
|
"New membership level": "Nouveau niveau d'appartenance au groupe",
|
||||||
"New message": "Nouveau message",
|
"New message": "Nouveau message",
|
||||||
|
"New message...": "Nouveau message...",
|
||||||
"New password": "Nouveau mot de passe",
|
"New password": "Nouveau mot de passe",
|
||||||
"New survey": "Nouveau sondage",
|
"New survey": "Nouveau sondage",
|
||||||
"Newest": "Plus récent",
|
"Newest": "Plus récent",
|
||||||
@ -350,6 +407,8 @@
|
|||||||
"Password required": "Mot de passe requis",
|
"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",
|
"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)",
|
"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 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 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 message content: ": "Veuillez entrer le contenu du message :",
|
||||||
@ -372,6 +431,9 @@
|
|||||||
"Question": "Question",
|
"Question": "Question",
|
||||||
"Question 1": "Question 1",
|
"Question 1": "Question 1",
|
||||||
"Question 2": "Question 2",
|
"Question 2": "Question 2",
|
||||||
|
"Ready": "Prêt",
|
||||||
|
"Record audio": "Faire un enregistrement audio",
|
||||||
|
"Recording...": "Enregistrement...",
|
||||||
"Reject": "Rejeter",
|
"Reject": "Rejeter",
|
||||||
"Reject request": "Rejeter la demande",
|
"Reject request": "Rejeter la demande",
|
||||||
"Remove": "Supprimer",
|
"Remove": "Supprimer",
|
||||||
@ -401,21 +463,26 @@
|
|||||||
"Sign out": "Déconnexion",
|
"Sign out": "Déconnexion",
|
||||||
"Specified email address was not found!": "L'adresse mail spécifiée n'a pas été trouvée !",
|
"Specified email address was not found!": "L'adresse mail spécifiée n'a pas été trouvée !",
|
||||||
"Specify URL": "Spécifier l'URL",
|
"Specify URL": "Spécifier l'URL",
|
||||||
|
"Statistics": "Statistiques",
|
||||||
"Stop streaming": "Arrêter de partager ma vidéo & mon audio",
|
"Stop streaming": "Arrêter de partager ma vidéo & mon audio",
|
||||||
"Submit": "Valider",
|
"Submit": "Valider",
|
||||||
"Switch camera": "Changer de caméra",
|
"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 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 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 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 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 !",
|
"The post has been successfully created!": "Le post a été créé avec succès !",
|
||||||
"There is no message yet in this converation.": "Il y n'a pas encore de message dans cette conversation.",
|
"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.",
|
"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 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 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 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 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 !",
|
"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 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...",
|
"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",
|
"Try again": "Essayer à nouveau",
|
||||||
@ -424,6 +491,7 @@
|
|||||||
"Update a conversation": "Modifier une conversation",
|
"Update a conversation": "Modifier une conversation",
|
||||||
"Update comment content": "Modifier le contenu du commentaire",
|
"Update comment content": "Modifier le contenu du commentaire",
|
||||||
"Update content": "Modifier le contenu",
|
"Update content": "Modifier le contenu",
|
||||||
|
"Update conversation": "Mise à jour d'une conversation",
|
||||||
"Update message": "Modifier un message",
|
"Update message": "Modifier un message",
|
||||||
"Update post content": "Modifier le contenu du post",
|
"Update post content": "Modifier le contenu du post",
|
||||||
"Update security questions": "Mise à jour des questions de sécurité",
|
"Update security questions": "Mise à jour des questions de sécurité",
|
||||||
@ -447,7 +515,7 @@
|
|||||||
"You security questions have been successfully updated!": "Vos questions de sécurité ont été mises avec succès !",
|
"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",
|
"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",
|
"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 et le gérer.",
|
"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 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 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 account image is visible only to connected Comunic users.": "Votre image de compte n'est accessible qu'aux personnes connectées.",
|
||||||
|
6
lib/constants.dart
Normal file
6
lib/constants.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/// Comunic mobile constants
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
/// Data serialisation directory
|
||||||
|
const SERIALIZATION_DIRECTORY = "serialization";
|
@ -60,23 +60,14 @@ class APIHelper {
|
|||||||
contentType: v.type,
|
contentType: v.type,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process picked files
|
|
||||||
for (final key in request.pickedFiles.keys) {
|
|
||||||
var v = request.pickedFiles[key];
|
|
||||||
data.files.add(MapEntry(
|
|
||||||
key,
|
|
||||||
MultipartFile.fromBytes(
|
|
||||||
await v.readAsBytes(),
|
|
||||||
filename: v.path.split("/").last,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the request
|
// Execute the request
|
||||||
final response = await Dio().post(
|
final response = await Dio().post(
|
||||||
url.toString(),
|
url.toString(),
|
||||||
data: data,
|
data: data,
|
||||||
|
cancelToken: request.cancelToken,
|
||||||
|
onSendProgress: request.progressCallback,
|
||||||
options: Options(
|
options: Options(
|
||||||
receiveDataWhenStatusError: true,
|
receiveDataWhenStatusError: true,
|
||||||
validateStatus: (s) => true,
|
validateStatus: (s) => true,
|
||||||
|
@ -17,7 +17,7 @@ class CommentsHelper {
|
|||||||
"content": comment.hasContent ? comment.content : "",
|
"content": comment.hasContent ? comment.content : "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (comment.hasImage) request.addPickedFile("image", comment.image);
|
if (comment.hasImage) request.addBytesFile("image", comment.image);
|
||||||
|
|
||||||
final response = await request.execWithFiles();
|
final response = await request.execWithFiles();
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:comunic/helpers/database/conversation_messages_database_helper.dart';
|
import 'package:comunic/helpers/serialization/conversation_message_serialization_helper.dart';
|
||||||
import 'package:comunic/helpers/database/conversations_database_helper.dart';
|
import 'package:comunic/helpers/serialization/conversations_serialization_helper.dart';
|
||||||
import 'package:comunic/helpers/users_helper.dart';
|
import 'package:comunic/helpers/users_helper.dart';
|
||||||
import 'package:comunic/helpers/websocket_helper.dart';
|
import 'package:comunic/helpers/websocket_helper.dart';
|
||||||
import 'package:comunic/lists/conversation_messages_list.dart';
|
import 'package:comunic/lists/conversation_messages_list.dart';
|
||||||
@ -9,11 +9,17 @@ import 'package:comunic/lists/users_list.dart';
|
|||||||
import 'package:comunic/models/api_request.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
import 'package:comunic/models/api_response.dart';
|
import 'package:comunic/models/api_response.dart';
|
||||||
import 'package:comunic/models/conversation.dart';
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
import 'package:comunic/models/conversation_member.dart';
|
||||||
import 'package:comunic/models/conversation_message.dart';
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
import 'package:comunic/models/displayed_content.dart';
|
import 'package:comunic/models/displayed_content.dart';
|
||||||
|
import 'package:comunic/models/new_conversation.dart';
|
||||||
import 'package:comunic/models/new_conversation_message.dart';
|
import 'package:comunic/models/new_conversation_message.dart';
|
||||||
|
import 'package:comunic/models/new_conversation_settings.dart';
|
||||||
import 'package:comunic/models/unread_conversation.dart';
|
import 'package:comunic/models/unread_conversation.dart';
|
||||||
import 'package:comunic/utils/account_utils.dart';
|
import 'package:comunic/utils/account_utils.dart';
|
||||||
|
import 'package:comunic/utils/color_utils.dart';
|
||||||
|
import 'package:comunic/utils/dart_color.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
/// Conversation helper
|
/// Conversation helper
|
||||||
@ -25,140 +31,147 @@ enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED }
|
|||||||
class ConversationsHelper {
|
class ConversationsHelper {
|
||||||
static final _registeredConversations = Map<int, int>();
|
static final _registeredConversations = Map<int, int>();
|
||||||
|
|
||||||
final ConversationsDatabaseHelper _conversationsDatabaseHelper =
|
|
||||||
ConversationsDatabaseHelper();
|
|
||||||
final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper =
|
|
||||||
ConversationMessagesDatabaseHelper();
|
|
||||||
|
|
||||||
/// Create a new conversation
|
/// Create a new conversation
|
||||||
///
|
///
|
||||||
/// Return the ID of the newly created conversation or -1 in case of failure
|
/// Return the ID of the newly created conversation
|
||||||
Future<int> createConversation(Conversation settings) async {
|
///
|
||||||
final response =
|
/// Throws in case of failure
|
||||||
await APIRequest(uri: "conversations/create", needLogin: true, args: {
|
static Future<int> createConversation(NewConversation settings) async {
|
||||||
"name": settings.hasName ? settings.name : "false",
|
final response = await APIRequest.withLogin("conversations/create", args: {
|
||||||
"follow": settings.following ? "true" : "false",
|
"name": settings.name ?? "",
|
||||||
|
"follow": settings.follow ? "true" : "false",
|
||||||
"users": settings.members.join(","),
|
"users": settings.members.join(","),
|
||||||
}).addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers).exec();
|
"color": colorToHex(settings.color)
|
||||||
|
})
|
||||||
if (response.code != 200) return -1;
|
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
return response.getObject()["conversationID"];
|
return response.getObject()["conversationID"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a member to a conversation.
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> addMember(int convID, int userID) async =>
|
||||||
|
await APIRequest.withLogin("conversations/addMember")
|
||||||
|
.addInt("convID", convID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Remove a member from a conversation.
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> removeMember(int convID, int userID) async =>
|
||||||
|
await APIRequest.withLogin("conversations/removeMember")
|
||||||
|
.addInt("convID", convID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
|
/// Update admin status of a user in a conversation
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
|
static Future<void> setAdmin(int convID, int userID, bool admin) async =>
|
||||||
|
await APIRequest.withLogin("conversations/setAdmin")
|
||||||
|
.addInt("convID", convID)
|
||||||
|
.addInt("userID", userID)
|
||||||
|
.addBool("setAdmin", admin)
|
||||||
|
.execWithThrow();
|
||||||
|
|
||||||
/// Update an existing conversation
|
/// Update an existing conversation
|
||||||
///
|
///
|
||||||
/// Returns a boolean depending of the success of the operation
|
/// Throws in case of failure
|
||||||
Future<bool> updateConversation(Conversation settings) async {
|
static Future<void> updateConversation(
|
||||||
final request =
|
NewConversationsSettings settings) async {
|
||||||
APIRequest(uri: "conversations/updateSettings", needLogin: true, args: {
|
final request = APIRequest.withLogin("conversations/updateSettings")
|
||||||
"conversationID": settings.id.toString(),
|
.addInt("conversationID", settings.convID)
|
||||||
"following": settings.following ? "true" : "false"
|
.addBool("following", settings.following);
|
||||||
});
|
|
||||||
|
|
||||||
if (settings.isOwner || settings.canEveryoneAddMembers)
|
// Update conversation settings
|
||||||
request.addString("members", settings.members.join(","));
|
if (settings.isComplete)
|
||||||
|
request
|
||||||
|
.addString("name", settings.name ?? "")
|
||||||
|
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
|
||||||
|
.addString("color", colorToHex(settings.color));
|
||||||
|
|
||||||
// Update all conversation settings, if possible
|
await request.execWithThrow();
|
||||||
if (settings.isOwner) {
|
|
||||||
request.addString("name", settings.hasName ? settings.name : "false");
|
// Delete old conversation entry from the database
|
||||||
request.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers);
|
await ConversationsSerializationHelper()
|
||||||
|
.removeElement((t) => t.id == settings.convID);
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await request.exec();
|
/// 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();
|
||||||
|
|
||||||
if (response.code != 200) return false;
|
/// Remove conversation logo
|
||||||
|
///
|
||||||
//Delete old conversation entry from the database
|
/// Throws in case of failure
|
||||||
await _conversationsDatabaseHelper.delete(settings.id);
|
static Future<void> removeLogo(int convID) async =>
|
||||||
|
await APIRequest.withLogin("conversations/delete_image")
|
||||||
// Success
|
.addInt("convID", convID)
|
||||||
return true;
|
.execWithThrow();
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a conversation specified by its [id]
|
/// Delete a conversation specified by its [id]
|
||||||
Future<bool> deleteConversation(int id) async {
|
Future<void> deleteConversation(int id) async =>
|
||||||
final response = await APIRequest(
|
await APIRequest.withLogin("conversations/delete")
|
||||||
uri: "conversations/delete",
|
.addInt("conversationID", id)
|
||||||
needLogin: true,
|
.execWithThrow();
|
||||||
args: {
|
|
||||||
"conversationID": id.toString(),
|
|
||||||
},
|
|
||||||
).exec();
|
|
||||||
|
|
||||||
return response.code == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Download the list of conversations from the server
|
/// Download the list of conversations from the server
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
Future<ConversationsList> downloadList() async {
|
Future<ConversationsList> downloadList() async {
|
||||||
final response =
|
final response =
|
||||||
await APIRequest(uri: "conversations/getList", needLogin: true).exec();
|
await APIRequest.withLogin("conversations/getList").execWithThrow();
|
||||||
|
|
||||||
if (response.code != 200) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
ConversationsList list = ConversationsList();
|
ConversationsList list = ConversationsList();
|
||||||
response.getArray().forEach((f) => list.add(apiToConversation(f)));
|
response.getArray().forEach((f) => list.add(apiToConversation(f)));
|
||||||
|
|
||||||
// Update the database
|
// Update the database
|
||||||
await _conversationsDatabaseHelper.clearTable();
|
await ConversationsSerializationHelper().setList(list);
|
||||||
await _conversationsDatabaseHelper.insertAll(list);
|
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
} on Exception catch (e) {
|
|
||||||
print(e.toString());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the local list of conversations
|
/// Get the local list of conversations
|
||||||
Future<ConversationsList> getCachedList() async {
|
Future<ConversationsList> getCachedList() async {
|
||||||
final list = await _conversationsDatabaseHelper.getAll();
|
final list = await ConversationsSerializationHelper().getList();
|
||||||
list.sort();
|
list.sort();
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get information about a single conversation specified by its [id]
|
/// Get information about a single conversation specified by its [id]
|
||||||
Future<Conversation> _downloadSingle(int id) async {
|
Future<Conversation> _downloadSingle(int id) async {
|
||||||
try {
|
|
||||||
final response = await APIRequest(
|
final response = await APIRequest(
|
||||||
uri: "conversations/getInfoOne",
|
uri: "conversations/get_single",
|
||||||
needLogin: true,
|
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;
|
return conversation;
|
||||||
} on Exception catch (e) {
|
|
||||||
print(e.toString());
|
|
||||||
print("Could not get information about a single conversation !");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get information about a single conversation. If [force] is set to false,
|
/// Get information about a conversation. If [force] is set to false, a
|
||||||
/// cached version of the conversation will be used, else it will always get
|
/// cached version of the conversation will be used, else it will always get
|
||||||
/// the information from the server
|
/// the information from the server. The method throws an [Exception] in
|
||||||
Future<Conversation> getSingle(int id, {bool force = false}) async {
|
|
||||||
if (force || !await _conversationsDatabaseHelper.has(id))
|
|
||||||
return await _downloadSingle(id);
|
|
||||||
else
|
|
||||||
return _conversationsDatabaseHelper.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get information about a conversation. The method throws an [Exception] in
|
|
||||||
/// case of failure
|
/// case of failure
|
||||||
///
|
///
|
||||||
/// Return value of this method is never null.
|
/// Return value of this method is never null.
|
||||||
Future<Conversation> getSingleOrThrow(int id, {bool force = false}) async {
|
Future<Conversation> getSingle(int id, {bool force = false}) async {
|
||||||
final conv = await this.getSingle(id, force: force);
|
if (force ||
|
||||||
|
!await ConversationsSerializationHelper().any((c) => c.id == id))
|
||||||
if (conv == null)
|
return await _downloadSingle(id);
|
||||||
throw Exception("Could not get information about the conversation!");
|
else
|
||||||
|
return await ConversationsSerializationHelper().get(id);
|
||||||
return conv;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the name of a [conversation]. This requires information
|
/// Get the name of a [conversation]. This requires information
|
||||||
@ -170,9 +183,9 @@ class ConversationsHelper {
|
|||||||
String name = "";
|
String name = "";
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < 3 && i < conversation.members.length; i++)
|
for (int i = 0; i < 3 && i < conversation.members.length; i++)
|
||||||
if (conversation.members[i] != userID()) {
|
if (conversation.members[i].userID != userID()) {
|
||||||
name += (count > 0 ? ", " : "") +
|
name += (count > 0 ? ", " : "") +
|
||||||
users.getUser(conversation.members[i]).fullName;
|
users.getUser(conversation.members[i].userID).fullName;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +197,8 @@ class ConversationsHelper {
|
|||||||
/// Search and return a private conversation with a given [userID]. If such
|
/// Search and return a private conversation with a given [userID]. If such
|
||||||
/// conversation does not exists, it is created if [allowCreate] is set to
|
/// conversation does not exists, it is created if [allowCreate] is set to
|
||||||
/// true
|
/// true
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
Future<int> getPrivate(int userID, {bool allowCreate = true}) async {
|
Future<int> getPrivate(int userID, {bool allowCreate = true}) async {
|
||||||
final response = await APIRequest(
|
final response = await APIRequest(
|
||||||
uri: "conversations/getPrivate",
|
uri: "conversations/getPrivate",
|
||||||
@ -192,17 +207,10 @@ class ConversationsHelper {
|
|||||||
"otherUser": userID.toString(),
|
"otherUser": userID.toString(),
|
||||||
"allowCreate": allowCreate.toString()
|
"allowCreate": allowCreate.toString()
|
||||||
},
|
},
|
||||||
).exec();
|
).execWithThrow();
|
||||||
|
|
||||||
if (response.code != 200) return null;
|
|
||||||
|
|
||||||
// Get and return conversation ID
|
// Get and return conversation ID
|
||||||
try {
|
|
||||||
return int.parse(response.getObject()["conversationsID"][0].toString());
|
return int.parse(response.getObject()["conversationsID"][0].toString());
|
||||||
} catch (e) {
|
|
||||||
e.toString();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asynchronously get the name of the conversation
|
/// Asynchronously get the name of the conversation
|
||||||
@ -210,15 +218,13 @@ class ConversationsHelper {
|
|||||||
/// Unlike the synchronous method, this method does not need information
|
/// Unlike the synchronous method, this method does not need information
|
||||||
/// about the members of the conversation
|
/// about the members of the conversation
|
||||||
///
|
///
|
||||||
/// Returns null in case of failure
|
/// Throws an exception in case of failure
|
||||||
static Future<String> getConversationNameAsync(
|
static Future<String> getConversationNameAsync(
|
||||||
Conversation conversation) async {
|
Conversation conversation) async {
|
||||||
if (conversation.hasName) return conversation.name;
|
if (conversation.hasName) return conversation.name;
|
||||||
|
|
||||||
//Get information about the members of the conversation
|
//Get information about the members of the conversation
|
||||||
final members = await UsersHelper().getUsersInfo(conversation.members);
|
final members = await UsersHelper().getList(conversation.membersID);
|
||||||
|
|
||||||
if (members == null) return null;
|
|
||||||
|
|
||||||
return ConversationsHelper.getConversationName(conversation, members);
|
return ConversationsHelper.getConversationName(conversation, members);
|
||||||
}
|
}
|
||||||
@ -226,14 +232,18 @@ class ConversationsHelper {
|
|||||||
/// Turn an API entry into a [Conversation] object
|
/// Turn an API entry into a [Conversation] object
|
||||||
static Conversation apiToConversation(Map<String, dynamic> map) {
|
static Conversation apiToConversation(Map<String, dynamic> map) {
|
||||||
return Conversation(
|
return Conversation(
|
||||||
id: map["ID"],
|
id: map["id"],
|
||||||
ownerID: map["ID_owner"],
|
lastActivity: map["last_activity"],
|
||||||
lastActive: map["last_active"],
|
name: map["name"],
|
||||||
name: map["name"] == false ? null : map["name"],
|
color: map["color"] == null ? null : HexColor(map["color"]),
|
||||||
following: map["following"] == 1,
|
logoURL: map["logo"],
|
||||||
sawLastMessage: map["saw_last_message"] == 1,
|
groupID: map["group_id"],
|
||||||
members: List<int>.from(map["members"]),
|
members: map["members"]
|
||||||
canEveryoneAddMembers: map["canEveryoneAddMembers"],
|
.cast<Map<String, dynamic>>()
|
||||||
|
.map(apiToConversationMember)
|
||||||
|
.toList()
|
||||||
|
.cast<ConversationMember>(),
|
||||||
|
canEveryoneAddMembers: map["can_everyone_add_members"],
|
||||||
callCapabilities: map["can_have_video_call"]
|
callCapabilities: map["can_have_video_call"]
|
||||||
? CallCapabilities.VIDEO
|
? CallCapabilities.VIDEO
|
||||||
: (map["can_have_call"]
|
: (map["can_have_call"]
|
||||||
@ -242,10 +252,21 @@ class ConversationsHelper {
|
|||||||
isHavingCall: map["has_call_now"]);
|
isHavingCall: map["has_call_now"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ConversationMember apiToConversationMember(Map<String, dynamic> map) =>
|
||||||
|
ConversationMember(
|
||||||
|
userID: map["user_id"],
|
||||||
|
lastMessageSeen: map["last_message_seen"],
|
||||||
|
lastAccessTime: map["last_access"],
|
||||||
|
following: map["following"],
|
||||||
|
isAdmin: map["is_admin"],
|
||||||
|
);
|
||||||
|
|
||||||
/// Parse a list of messages given by the server
|
/// Parse a list of messages given by the server
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
Future<ConversationMessagesList> _parseConversationMessageFromServer(
|
Future<ConversationMessagesList> _parseConversationMessageFromServer(
|
||||||
int conversationID, APIResponse response) async {
|
int conversationID, APIResponse response) async {
|
||||||
if (response.code != 200) return null;
|
response.assertOk();
|
||||||
|
|
||||||
// Parse the response of the server
|
// Parse the response of the server
|
||||||
ConversationMessagesList list = ConversationMessagesList();
|
ConversationMessagesList list = ConversationMessagesList();
|
||||||
@ -256,7 +277,8 @@ class ConversationsHelper {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Save messages in the cache
|
// Save messages in the cache
|
||||||
_conversationMessagesDatabaseHelper.insertOrUpdateAll(list);
|
await ConversationsMessagesSerializationHelper(conversationID)
|
||||||
|
.insertOrReplaceAll(list);
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@ -265,6 +287,8 @@ class ConversationsHelper {
|
|||||||
///
|
///
|
||||||
/// Set [lastMessageID] to 0 to specify that we do not have any message of the
|
/// Set [lastMessageID] to 0 to specify that we do not have any message of the
|
||||||
/// conversation yet or another value else
|
/// conversation yet or another value else
|
||||||
|
///
|
||||||
|
/// Throws an exception in case of failure
|
||||||
Future<ConversationMessagesList> _downloadNewMessagesSingle(
|
Future<ConversationMessagesList> _downloadNewMessagesSingle(
|
||||||
int conversationID,
|
int conversationID,
|
||||||
{int lastMessageID = 0}) async {
|
{int lastMessageID = 0}) async {
|
||||||
@ -275,26 +299,26 @@ class ConversationsHelper {
|
|||||||
args: {
|
args: {
|
||||||
"conversationID": conversationID.toString(),
|
"conversationID": conversationID.toString(),
|
||||||
"last_message_id": lastMessageID.toString()
|
"last_message_id": lastMessageID.toString()
|
||||||
}).exec();
|
}).execWithThrow();
|
||||||
|
|
||||||
return await _parseConversationMessageFromServer(conversationID, response);
|
return await _parseConversationMessageFromServer(conversationID, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get older messages for a given conversation from an online source
|
/// Get older messages for a given conversation from an online source
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
Future<ConversationMessagesList> getOlderMessages({
|
Future<ConversationMessagesList> getOlderMessages({
|
||||||
@required int conversationID,
|
@required int conversationID,
|
||||||
@required int oldestMessagesID,
|
@required int oldestMessagesID,
|
||||||
int limit = 15,
|
int limit = 15,
|
||||||
}) async {
|
}) async {
|
||||||
// Perform the request online
|
// Perform the request online
|
||||||
final response = await APIRequest(
|
final response =
|
||||||
uri: "conversations/get_older_messages",
|
await APIRequest.withLogin("conversations/get_older_messages", args: {
|
||||||
needLogin: true,
|
|
||||||
args: {
|
|
||||||
"conversationID": conversationID.toString(),
|
"conversationID": conversationID.toString(),
|
||||||
"oldest_message_id": oldestMessagesID.toString(),
|
"oldest_message_id": oldestMessagesID.toString(),
|
||||||
"limit": limit.toString()
|
"limit": limit.toString()
|
||||||
}).exec();
|
}).execWithThrow();
|
||||||
|
|
||||||
return await _parseConversationMessageFromServer(conversationID, response);
|
return await _parseConversationMessageFromServer(conversationID, response);
|
||||||
}
|
}
|
||||||
@ -304,6 +328,8 @@ class ConversationsHelper {
|
|||||||
/// If [lastMessageID] is set to 0 then we retrieve the last messages of
|
/// If [lastMessageID] is set to 0 then we retrieve the last messages of
|
||||||
/// the conversation.
|
/// the conversation.
|
||||||
/// Otherwise [lastMessageID] contains the ID of the last known message
|
/// Otherwise [lastMessageID] contains the ID of the last known message
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
Future<ConversationMessagesList> getNewMessages(
|
Future<ConversationMessagesList> getNewMessages(
|
||||||
{@required int conversationID,
|
{@required int conversationID,
|
||||||
int lastMessageID = 0,
|
int lastMessageID = 0,
|
||||||
@ -312,35 +338,32 @@ class ConversationsHelper {
|
|||||||
return await _downloadNewMessagesSingle(conversationID,
|
return await _downloadNewMessagesSingle(conversationID,
|
||||||
lastMessageID: lastMessageID);
|
lastMessageID: lastMessageID);
|
||||||
else
|
else
|
||||||
return await _conversationMessagesDatabaseHelper
|
return await ConversationsMessagesSerializationHelper(conversationID)
|
||||||
.getAllMessagesConversations(conversationID,
|
.getList();
|
||||||
lastMessageID: lastMessageID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a single conversation message from the local database
|
|
||||||
///
|
|
||||||
/// Returns the message if found or null in case of failure
|
|
||||||
Future<ConversationMessage> getSingleMessageFromCache(int messageID) async {
|
|
||||||
return await _conversationMessagesDatabaseHelper.get(messageID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a new message to the server
|
/// Send a new message to the server
|
||||||
Future<SendMessageResult> sendMessage(NewConversationMessage message) async {
|
Future<SendMessageResult> sendMessage(
|
||||||
final request = APIRequest(
|
NewConversationMessage message, {
|
||||||
uri: "conversations/sendMessage",
|
ProgressCallback sendProgress,
|
||||||
needLogin: true,
|
CancelToken cancelToken,
|
||||||
args: {
|
}) async {
|
||||||
"conversationID": message.conversationID.toString(),
|
final request = APIRequest.withLogin("conversations/sendMessage")
|
||||||
"message": message.hasMessage ? message.message : ""
|
.addInt("conversationID", message.conversationID)
|
||||||
},
|
.addString("message", message.hasMessage ? message.message : "");
|
||||||
);
|
|
||||||
|
|
||||||
// Check for image
|
request.progressCallback = sendProgress;
|
||||||
if (message.hasImage) request.addPickedFile("image", message.image);
|
request.cancelToken = cancelToken;
|
||||||
|
|
||||||
|
// Check for file
|
||||||
|
if (message.hasFile) request.addBytesFile("file", message.file);
|
||||||
|
|
||||||
|
if (message.hasThumbnail)
|
||||||
|
request.addBytesFile("thumbnail", message.thumbnail);
|
||||||
|
|
||||||
//Send the message
|
//Send the message
|
||||||
APIResponse response;
|
APIResponse response;
|
||||||
if (!message.hasImage)
|
if (!message.hasFile)
|
||||||
response = await request.exec();
|
response = await request.exec();
|
||||||
else
|
else
|
||||||
response = await request.execWithFiles();
|
response = await request.execWithFiles();
|
||||||
@ -353,14 +376,13 @@ class ConversationsHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Save / Update a message into the database
|
/// Save / Update a message into the database
|
||||||
Future<void> saveMessage(ConversationMessage msg) async {
|
Future<void> saveMessage(ConversationMessage msg) async =>
|
||||||
await _conversationMessagesDatabaseHelper.insertOrUpdate(msg);
|
await ConversationsMessagesSerializationHelper(msg.convID)
|
||||||
}
|
.insertOrReplace(msg);
|
||||||
|
|
||||||
/// Remove a message from the database
|
/// Remove a message from the database
|
||||||
Future<void> removeMessage(int msgID) async {
|
Future<void> removeMessage(ConversationMessage msg) async =>
|
||||||
await _conversationMessagesDatabaseHelper.delete(msgID);
|
await ConversationsMessagesSerializationHelper(msg.convID).remove(msg);
|
||||||
}
|
|
||||||
|
|
||||||
/// Update a message content
|
/// Update a message content
|
||||||
Future<bool> updateMessage(int id, String newContent) async {
|
Future<bool> updateMessage(int id, String newContent) async {
|
||||||
@ -397,11 +419,8 @@ class ConversationsHelper {
|
|||||||
|
|
||||||
return UnreadConversationsList()
|
return UnreadConversationsList()
|
||||||
..addAll(list.map((f) => UnreadConversation(
|
..addAll(list.map((f) => UnreadConversation(
|
||||||
id: f["id"],
|
conv: apiToConversation(f["conv"]),
|
||||||
convName: f["conv_name"],
|
message: apiToConversationMessage(f["message"]),
|
||||||
lastActive: f["last_active"],
|
|
||||||
userID: f["userID"],
|
|
||||||
message: f["message"],
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,17 +447,70 @@ class ConversationsHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a notification to inform that the user is writing a message
|
||||||
|
static Future<void> sendWritingEvent(int convID) async =>
|
||||||
|
await ws("conversations/is_writing", {"convID": convID});
|
||||||
|
|
||||||
/// Turn an API response into a ConversationMessage object
|
/// Turn an API response into a ConversationMessage object
|
||||||
static ConversationMessage apiToConversationMessage(
|
static ConversationMessage apiToConversationMessage(
|
||||||
Map<String, dynamic> map,
|
Map<String, dynamic> map,
|
||||||
) {
|
) {
|
||||||
return ConversationMessage(
|
var file;
|
||||||
id: map["ID"],
|
if (map["file"] != null) {
|
||||||
conversationID: map["convID"],
|
final fileMap = map["file"];
|
||||||
userID: map["ID_user"],
|
file = ConversationMessageFile(
|
||||||
timeInsert: map["time_insert"],
|
url: fileMap["url"],
|
||||||
message: DisplayedString(map["message"]),
|
size: fileMap["size"],
|
||||||
imageURL: map["image_path"],
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +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';
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -25,30 +25,6 @@ abstract class UserTableContract {
|
|||||||
static const C_CUSTOM_EMOJIES = "custom_emojies";
|
static const C_CUSTOM_EMOJIES = "custom_emojies";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Conversations table contract
|
|
||||||
abstract class ConversationTableContract {
|
|
||||||
static const TABLE_NAME = "conversations";
|
|
||||||
static const C_ID = BaseTableContract.C_ID;
|
|
||||||
static const C_OWNER_ID = "owner_id";
|
|
||||||
static const C_LAST_ACTIVE = "last_active";
|
|
||||||
static const C_NAME = "name";
|
|
||||||
static const C_FOLLOWING = "following";
|
|
||||||
static const C_SAW_LAST_MESSAGE = "saw_last_message";
|
|
||||||
static const C_MEMBERS = "members";
|
|
||||||
static const C_CAN_EVERYONE_ADD_MEMBERS = "can_everyone_add_members";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Conversations messages table contract
|
|
||||||
abstract class ConversationsMessagesTableContract {
|
|
||||||
static const TABLE_NAME = "conversations_messages";
|
|
||||||
static const C_ID = BaseTableContract.C_ID;
|
|
||||||
static const C_CONVERSATION_ID = "conversation_id";
|
|
||||||
static const C_USER_ID = "user_id";
|
|
||||||
static const C_TIME_INSERT = "time_insert";
|
|
||||||
static const C_MESSAGE = "message";
|
|
||||||
static const C_IMAGE_URL = "image_url";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Friends table contract
|
/// Friends table contract
|
||||||
abstract class FriendsListTableContract {
|
abstract class FriendsListTableContract {
|
||||||
static const TABLE_NAME = "friends";
|
static const TABLE_NAME = "friends";
|
||||||
|
@ -45,14 +45,6 @@ abstract class DatabaseHelper {
|
|||||||
// Drop users table
|
// Drop users table
|
||||||
await db.execute("DROP TABLE IF EXISTS ${UserTableContract.TABLE_NAME}");
|
await db.execute("DROP TABLE IF EXISTS ${UserTableContract.TABLE_NAME}");
|
||||||
|
|
||||||
// Drop conversations table
|
|
||||||
await db.execute(
|
|
||||||
"DROP TABLE IF EXISTS ${ConversationTableContract.TABLE_NAME}");
|
|
||||||
|
|
||||||
// Drop conversations messages table
|
|
||||||
await db.execute(
|
|
||||||
"DROP TABLE IF EXISTS ${ConversationsMessagesTableContract.TABLE_NAME}");
|
|
||||||
|
|
||||||
// Drop friends list table
|
// Drop friends list table
|
||||||
await db
|
await db
|
||||||
.execute("DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}");
|
.execute("DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}");
|
||||||
@ -74,29 +66,6 @@ abstract class DatabaseHelper {
|
|||||||
"${UserTableContract.C_CUSTOM_EMOJIES} TEXT"
|
"${UserTableContract.C_CUSTOM_EMOJIES} TEXT"
|
||||||
")");
|
")");
|
||||||
|
|
||||||
// Create conversations table
|
|
||||||
await db.execute("CREATE TABLE ${ConversationTableContract.TABLE_NAME} ("
|
|
||||||
"${ConversationTableContract.C_ID} INTEGER PRIMARY KEY, "
|
|
||||||
"${ConversationTableContract.C_OWNER_ID} INTEGER, "
|
|
||||||
"${ConversationTableContract.C_LAST_ACTIVE} INTEGER, "
|
|
||||||
"${ConversationTableContract.C_NAME} TEXT, "
|
|
||||||
"${ConversationTableContract.C_FOLLOWING} INTEGER, "
|
|
||||||
"${ConversationTableContract.C_SAW_LAST_MESSAGE} INTEGER, "
|
|
||||||
"${ConversationTableContract.C_MEMBERS} TEXT, "
|
|
||||||
"${ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS} INTEGER"
|
|
||||||
")");
|
|
||||||
|
|
||||||
// Create conversation messages table
|
|
||||||
await db.execute(
|
|
||||||
"CREATE TABLE ${ConversationsMessagesTableContract.TABLE_NAME} ("
|
|
||||||
"${ConversationsMessagesTableContract.C_ID} INTEGER PRIMARY KEY, "
|
|
||||||
"${ConversationsMessagesTableContract.C_CONVERSATION_ID} INTEGER, "
|
|
||||||
"${ConversationsMessagesTableContract.C_USER_ID} INTEGER, "
|
|
||||||
"${ConversationsMessagesTableContract.C_TIME_INSERT} INTEGER, "
|
|
||||||
"${ConversationsMessagesTableContract.C_MESSAGE} TEXT, "
|
|
||||||
"${ConversationsMessagesTableContract.C_IMAGE_URL} TEXT"
|
|
||||||
")");
|
|
||||||
|
|
||||||
// Friends list table
|
// Friends list table
|
||||||
await db.execute("CREATE TABLE ${FriendsListTableContract.TABLE_NAME} ("
|
await db.execute("CREATE TABLE ${FriendsListTableContract.TABLE_NAME} ("
|
||||||
"${FriendsListTableContract.C_ID} INTEGER PRIMARY KEY, "
|
"${FriendsListTableContract.C_ID} INTEGER PRIMARY KEY, "
|
||||||
|
@ -50,6 +50,14 @@ class DeletedCommentEvent {
|
|||||||
DeletedCommentEvent(this.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
|
/// New conversation message
|
||||||
class NewConversationMessageEvent {
|
class NewConversationMessageEvent {
|
||||||
final ConversationMessage msg;
|
final ConversationMessage msg;
|
||||||
@ -71,6 +79,21 @@ class DeletedConversationMessageEvent {
|
|||||||
DeletedConversationMessageEvent(this.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
|
/// User joined call event
|
||||||
class UserJoinedCallEvent {
|
class UserJoinedCallEvent {
|
||||||
final int callID;
|
final int callID;
|
||||||
|
@ -142,7 +142,7 @@ class PostsHelper {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case PostKind.IMAGE:
|
case PostKind.IMAGE:
|
||||||
request.addPickedFile("image", post.image);
|
request.addBytesFile("image", post.image);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PostKind.WEB_LINK:
|
case PostKind.WEB_LINK:
|
||||||
|
121
lib/helpers/serialization/base_serialization_helper.dart
Normal file
121
lib/helpers/serialization/base_serialization_helper.dart
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:comunic/constants.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
/// Base serialization helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
abstract class SerializableElement<T> extends Comparable<T> {
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseSerializationHelper<T extends SerializableElement> {
|
||||||
|
/// List cache
|
||||||
|
List<T> _cache;
|
||||||
|
|
||||||
|
/// The name of the type of data to serialise
|
||||||
|
String get type;
|
||||||
|
|
||||||
|
/// Parse an json entry into a [T] object
|
||||||
|
T parse(Map<String, dynamic> m);
|
||||||
|
|
||||||
|
/// Get the file where data should be stored
|
||||||
|
Future<File> _getFilePath() async {
|
||||||
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
final targetDir =
|
||||||
|
Directory(path.join(dir.absolute.path, SERIALIZATION_DIRECTORY));
|
||||||
|
|
||||||
|
targetDir.create(recursive: true);
|
||||||
|
|
||||||
|
return File(path.join(targetDir.absolute.path, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the cache
|
||||||
|
Future<void> _loadCache() async {
|
||||||
|
if (_cache != null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final file = await _getFilePath();
|
||||||
|
|
||||||
|
if (!await file.exists()) return _cache = [];
|
||||||
|
|
||||||
|
final List<dynamic> json = jsonDecode(await file.readAsString());
|
||||||
|
_cache = json.cast<Map<String, dynamic>>().map(parse).toList();
|
||||||
|
|
||||||
|
_cache.sort();
|
||||||
|
} catch (e, s) {
|
||||||
|
print("Failed to read serialized data! $e => $s");
|
||||||
|
_cache = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the cache to the persistent memory
|
||||||
|
Future<void> _saveCache() async {
|
||||||
|
final file = await _getFilePath();
|
||||||
|
await file.writeAsString(jsonEncode(
|
||||||
|
_cache.map((e) => e.toJson()).toList().cast<Map<String, dynamic>>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current list of elements
|
||||||
|
Future<List<T>> getList() async {
|
||||||
|
await _loadCache();
|
||||||
|
return List.from(_cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a new list of conversations
|
||||||
|
Future<void> setList(List<T> list) async {
|
||||||
|
_cache = List.from(list);
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert new element
|
||||||
|
Future<void> insert(T el) async {
|
||||||
|
await _loadCache();
|
||||||
|
_cache.add(el);
|
||||||
|
_cache.sort();
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert new element
|
||||||
|
Future<void> insertMany(List<T> els) async {
|
||||||
|
await _loadCache();
|
||||||
|
_cache.addAll(els);
|
||||||
|
_cache.sort();
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if any entry in the last match the predicate
|
||||||
|
Future<bool> any(bool isContained(T t)) async {
|
||||||
|
await _loadCache();
|
||||||
|
return _cache.any((element) => isContained(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if any entry in the last match the predicate
|
||||||
|
Future<T> first(bool filter(T t)) async {
|
||||||
|
await _loadCache();
|
||||||
|
return _cache.firstWhere((element) => filter(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace an element with another one
|
||||||
|
Future<void> insertOrReplaceElement(bool isToReplace(T t), T newEl) async {
|
||||||
|
await _loadCache();
|
||||||
|
|
||||||
|
// Insert or replace the element
|
||||||
|
_cache.where((element) => !isToReplace(element)).toList();
|
||||||
|
_cache.add(newEl);
|
||||||
|
|
||||||
|
_cache.sort();
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove elements
|
||||||
|
Future<void> removeElement(bool isToRemove(T t)) async {
|
||||||
|
await _loadCache();
|
||||||
|
_cache.removeWhere((element) => isToRemove(element));
|
||||||
|
await _saveCache();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
|
||||||
|
import 'package:comunic/lists/conversation_messages_list.dart';
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
|
||||||
|
/// Conversations serialization helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
HashMap<int, ConversationsMessagesSerializationHelper> _instances;
|
||||||
|
|
||||||
|
class ConversationsMessagesSerializationHelper
|
||||||
|
extends BaseSerializationHelper<ConversationMessage> {
|
||||||
|
final int convID;
|
||||||
|
|
||||||
|
ConversationsMessagesSerializationHelper._(int convID)
|
||||||
|
: convID = convID,
|
||||||
|
assert(convID != null);
|
||||||
|
|
||||||
|
factory ConversationsMessagesSerializationHelper(int convID) {
|
||||||
|
if (_instances == null) _instances = HashMap();
|
||||||
|
|
||||||
|
if (!_instances.containsKey(convID))
|
||||||
|
_instances[convID] = ConversationsMessagesSerializationHelper._(convID);
|
||||||
|
|
||||||
|
return _instances[convID];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConversationMessage parse(Map<String, dynamic> m) =>
|
||||||
|
ConversationMessage.fromJson(m);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get type => "conv-messages-$convID";
|
||||||
|
|
||||||
|
Future<ConversationMessagesList> getList() async =>
|
||||||
|
ConversationMessagesList()..addAll(await super.getList());
|
||||||
|
|
||||||
|
Future<void> insertOrReplace(ConversationMessage msg) async =>
|
||||||
|
await insertOrReplaceElement((t) => t.id == msg.id, msg);
|
||||||
|
|
||||||
|
Future<void> remove(ConversationMessage msg) async =>
|
||||||
|
await removeElement((t) => t.id == msg.id);
|
||||||
|
|
||||||
|
/// Insert or replace a list of messages
|
||||||
|
Future<void> insertOrReplaceAll(List<ConversationMessage> list) async {
|
||||||
|
for (var message in list)
|
||||||
|
await insertOrReplaceElement((t) => t.id == message.id, message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
|
||||||
|
import 'package:comunic/lists/conversations_list.dart';
|
||||||
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
|
||||||
|
/// Conversations serialization helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
var _cache;
|
||||||
|
|
||||||
|
class ConversationsSerializationHelper
|
||||||
|
extends BaseSerializationHelper<Conversation> {
|
||||||
|
/// Singleton
|
||||||
|
factory ConversationsSerializationHelper() {
|
||||||
|
if (_cache == null) _cache = ConversationsSerializationHelper._();
|
||||||
|
return _cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConversationsSerializationHelper._();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Conversation parse(Map<String, dynamic> m) => Conversation.fromJson(m);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get type => "conversations";
|
||||||
|
|
||||||
|
Future<ConversationsList> getList() async =>
|
||||||
|
ConversationsList()..addAll(await super.getList());
|
||||||
|
|
||||||
|
/// Get a conversation
|
||||||
|
Future<Conversation> get(int id) => first((t) => t.id == id);
|
||||||
|
}
|
@ -19,6 +19,7 @@ class ServerConfigurationHelper {
|
|||||||
|
|
||||||
final passwordPolicy = response["password_policy"];
|
final passwordPolicy = response["password_policy"];
|
||||||
final dataConservationPolicy = response["data_conservation_policy"];
|
final dataConservationPolicy = response["data_conservation_policy"];
|
||||||
|
final conversationsPolicy = response["conversations_policy"];
|
||||||
|
|
||||||
_config = ServerConfig(
|
_config = ServerConfig(
|
||||||
minSupportedMobileVersion:
|
minSupportedMobileVersion:
|
||||||
@ -50,7 +51,22 @@ class ServerConfigurationHelper {
|
|||||||
dataConservationPolicy["min_conversation_messages_lifetime"],
|
dataConservationPolicy["min_conversation_messages_lifetime"],
|
||||||
minLikesLifetime: dataConservationPolicy["min_likes_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
|
/// Get current server configuration, throwing if it is not loaded yet
|
||||||
@ -62,3 +78,6 @@ class ServerConfigurationHelper {
|
|||||||
return _config;
|
return _config;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shortcut for server configuration
|
||||||
|
ServerConfig get srvConfig => ServerConfigurationHelper.config;
|
||||||
|
@ -5,7 +5,8 @@ import 'package:comunic/models/data_conservation_policy_settings.dart';
|
|||||||
import 'package:comunic/models/general_settings.dart';
|
import 'package:comunic/models/general_settings.dart';
|
||||||
import 'package:comunic/models/new_emoji.dart';
|
import 'package:comunic/models/new_emoji.dart';
|
||||||
import 'package:comunic/models/security_settings.dart';
|
import 'package:comunic/models/security_settings.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
|
import '../models/api_request.dart';
|
||||||
|
|
||||||
/// Settings helper
|
/// Settings helper
|
||||||
///
|
///
|
||||||
@ -92,11 +93,10 @@ class SettingsHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Upload a new account image
|
/// Upload a new account image
|
||||||
static Future<bool> uploadAccountImage(PickedFile newImage) async =>
|
static Future<void> uploadAccountImage(BytesFile newImage) async =>
|
||||||
(await APIRequest(uri: "settings/upload_account_image", needLogin: true)
|
await APIRequest(uri: "settings/upload_account_image", needLogin: true)
|
||||||
.addPickedFile("picture", newImage)
|
.addBytesFile("picture", newImage)
|
||||||
.execWithFiles())
|
.execWithFilesAndThrow();
|
||||||
.isOK;
|
|
||||||
|
|
||||||
/// Upload a new account image from memory
|
/// Upload a new account image from memory
|
||||||
static Future<bool> uploadAccountImageFromMemory(List<int> bytes) async =>
|
static Future<bool> uploadAccountImageFromMemory(List<int> bytes) async =>
|
||||||
@ -128,13 +128,12 @@ class SettingsHelper {
|
|||||||
|
|
||||||
/// Upload a new custom emoji
|
/// Upload a new custom emoji
|
||||||
static Future<void> uploadNewCustomEmoji(NewEmoji newEmoji) async =>
|
static Future<void> uploadNewCustomEmoji(NewEmoji newEmoji) async =>
|
||||||
(await APIRequest(
|
await APIRequest(
|
||||||
uri: "settings/upload_custom_emoji",
|
uri: "settings/upload_custom_emoji",
|
||||||
needLogin: true,
|
needLogin: true,
|
||||||
args: {"shortcut": newEmoji.shortcut})
|
args: {"shortcut": newEmoji.shortcut})
|
||||||
.addPickedFile("image", newEmoji.image)
|
.addBytesFile("image", newEmoji.image)
|
||||||
.execWithFiles())
|
.execWithFilesAndThrow();
|
||||||
.assertOk();
|
|
||||||
|
|
||||||
/// Delete a custom emoji
|
/// Delete a custom emoji
|
||||||
///
|
///
|
||||||
@ -220,7 +219,8 @@ class SettingsHelper {
|
|||||||
/// Throws in case of failure
|
/// Throws in case of failure
|
||||||
static Future<void> setDataConservationPolicy(
|
static Future<void> setDataConservationPolicy(
|
||||||
String password, DataConservationPolicySettings newSettings) async {
|
String password, DataConservationPolicySettings newSettings) async {
|
||||||
await APIRequest(uri: "settings/set_data_conservation_policy", needLogin: true)
|
await APIRequest(
|
||||||
|
uri: "settings/set_data_conservation_policy", needLogin: true)
|
||||||
.addString("password", password)
|
.addString("password", password)
|
||||||
.addInt("inactive_account_lifetime",
|
.addInt("inactive_account_lifetime",
|
||||||
newSettings.inactiveAccountLifeTime ?? 0)
|
newSettings.inactiveAccountLifeTime ?? 0)
|
||||||
|
@ -91,9 +91,16 @@ class UsersHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get users information from a given [Set]
|
/// Get users information from a given [Set]
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
Future<UsersList> getList(Set<int> users,
|
Future<UsersList> getList(Set<int> users,
|
||||||
{bool forceDownload = false}) async {
|
{bool forceDownload = false}) async {
|
||||||
return await getUsersInfo(users.toList());
|
final list = await getUsersInfo(users.toList());
|
||||||
|
|
||||||
|
if (list == null)
|
||||||
|
throw Exception("Failed to get the list of users!");
|
||||||
|
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get users information
|
/// Get users information
|
||||||
@ -102,7 +109,7 @@ class UsersHelper {
|
|||||||
/// the server, otherwise cached data will be used if available
|
/// the server, otherwise cached data will be used if available
|
||||||
Future<UsersList> getUsersInfo(List<int> users,
|
Future<UsersList> getUsersInfo(List<int> users,
|
||||||
{bool forceDownload = false}) async {
|
{bool forceDownload = false}) async {
|
||||||
List<int> toDownload = List();
|
List<int> toDownload = [];
|
||||||
UsersList list = UsersList();
|
UsersList list = UsersList();
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
|
@ -144,6 +144,12 @@ class WebSocketHelper {
|
|||||||
EventsHelper.emit(DeletedCommentEvent(msg.data));
|
EventsHelper.emit(DeletedCommentEvent(msg.data));
|
||||||
break;
|
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
|
// Created new conversation message
|
||||||
case "new_conv_message":
|
case "new_conv_message":
|
||||||
EventsHelper.emit(NewConversationMessageEvent(
|
EventsHelper.emit(NewConversationMessageEvent(
|
||||||
@ -162,6 +168,17 @@ class WebSocketHelper {
|
|||||||
ConversationsHelper.apiToConversationMessage(msg.data)));
|
ConversationsHelper.apiToConversationMessage(msg.data)));
|
||||||
break;
|
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
|
// A user joined a call
|
||||||
case "user_joined_call":
|
case "user_joined_call":
|
||||||
EventsHelper.emit(
|
EventsHelper.emit(
|
||||||
|
@ -5,7 +5,7 @@ import 'dart:collection';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class AbstractList<E> extends ListBase<E> {
|
class AbstractList<E> extends ListBase<E> {
|
||||||
final _list = List<E>();
|
final _list = <E>[];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import 'package:comunic/models/comment.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class CommentsList extends ListBase<Comment> {
|
class CommentsList extends ListBase<Comment> {
|
||||||
List<Comment> _list = List();
|
List<Comment> _list = [];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import 'package:comunic/models/conversation_message.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class ConversationMessagesList extends ListBase<ConversationMessage> {
|
class ConversationMessagesList extends ListBase<ConversationMessage> {
|
||||||
final List<ConversationMessage> _list = List();
|
final List<ConversationMessage> _list = [];
|
||||||
|
|
||||||
set length(int v) => _list.length = v;
|
set length(int v) => _list.length = v;
|
||||||
|
|
||||||
@ -24,11 +24,10 @@ class ConversationMessagesList extends ListBase<ConversationMessage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the list of the users ID who own a message in this list
|
/// Get the list of the users ID who own a message in this list
|
||||||
List<int> getUsersID() {
|
Set<int> getUsersID() {
|
||||||
final List<int> users = List();
|
final Set<int> users = Set();
|
||||||
|
|
||||||
for (ConversationMessage message in this)
|
for (ConversationMessage message in this) users.addAll(message.usersID);
|
||||||
if (!users.contains(message.userID)) users.add(message.userID);
|
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,11 @@ import 'package:comunic/models/conversation.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class ConversationsList extends ListBase<Conversation> {
|
class ConversationsList extends ListBase<Conversation> {
|
||||||
|
final List<Conversation> _list = [];
|
||||||
final List<Conversation> _list = List();
|
|
||||||
UsersList users;
|
UsersList users;
|
||||||
|
|
||||||
set length(l) => _list.length = l;
|
set length(l) => _list.length = l;
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -22,12 +22,9 @@ class ConversationsList extends ListBase<Conversation> {
|
|||||||
void operator []=(int index, Conversation value) => _list[index] = value;
|
void operator []=(int index, Conversation value) => _list[index] = value;
|
||||||
|
|
||||||
/// Get the entire lists of users ID in this list
|
/// Get the entire lists of users ID in this list
|
||||||
List<int> get allUsersID {
|
Set<int> get allUsersID {
|
||||||
final List<int> list = List();
|
final Set<int> list = Set();
|
||||||
forEach((c) => c.members.forEach((id){
|
forEach((c) => c.members.forEach((member) => list.add(member.userID)));
|
||||||
if(!list.contains(id))
|
|
||||||
list.add(id);
|
|
||||||
}));
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import 'package:comunic/models/friend.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class FriendsList extends ListBase<Friend> {
|
class FriendsList extends ListBase<Friend> {
|
||||||
List<Friend> _list = List();
|
List<Friend> _list = [];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class MembershipList extends AbstractList<Membership> {
|
|||||||
case MembershipType.GROUP:
|
case MembershipType.GROUP:
|
||||||
break;
|
break;
|
||||||
case MembershipType.CONVERSATION:
|
case MembershipType.CONVERSATION:
|
||||||
s.addAll(m.conversation.members);
|
s.addAll(m.conversation.membersID);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ import 'package:comunic/models/post.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class PostsList extends ListBase<Post> {
|
class PostsList extends ListBase<Post> {
|
||||||
List<Post> _list = List();
|
List<Post> _list = [];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
@ -7,5 +7,9 @@ import 'package:comunic/models/unread_conversation.dart';
|
|||||||
|
|
||||||
class UnreadConversationsList extends AbstractList<UnreadConversation> {
|
class UnreadConversationsList extends AbstractList<UnreadConversation> {
|
||||||
/// Get the ID of the users included in this list
|
/// Get the ID of the users included in this list
|
||||||
Set<int> get usersID => new Set<int>()..addAll(map((f) => f.userID));
|
Set<int> get usersID {
|
||||||
|
final set = Set<int>();
|
||||||
|
forEach((element) => set.addAll(element.message.usersID));
|
||||||
|
return set;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import 'package:comunic/models/user.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class UsersList extends ListBase<User> {
|
class UsersList extends ListBase<User> {
|
||||||
List<User> _list = List();
|
List<User> _list = [];
|
||||||
|
|
||||||
int get length => _list.length;
|
int get length => _list.length;
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:comunic/helpers/api_helper.dart';
|
import 'package:comunic/helpers/api_helper.dart';
|
||||||
import 'package:comunic/models/api_response.dart';
|
import 'package:comunic/models/api_response.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
/// API Request model
|
/// API Request model
|
||||||
@ -27,9 +27,10 @@ class BytesFile {
|
|||||||
class APIRequest {
|
class APIRequest {
|
||||||
final String uri;
|
final String uri;
|
||||||
final bool needLogin;
|
final bool needLogin;
|
||||||
|
ProgressCallback progressCallback;
|
||||||
|
CancelToken cancelToken;
|
||||||
Map<String, String> args;
|
Map<String, String> args;
|
||||||
Map<String, File> files = Map();
|
Map<String, File> files = Map();
|
||||||
Map<String, PickedFile> pickedFiles = Map();
|
|
||||||
Map<String, BytesFile> bytesFiles = Map();
|
Map<String, BytesFile> bytesFiles = Map();
|
||||||
|
|
||||||
APIRequest({@required this.uri, this.needLogin = false, this.args})
|
APIRequest({@required this.uri, this.needLogin = false, this.args})
|
||||||
@ -70,11 +71,6 @@ class APIRequest {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
APIRequest addPickedFile(String name, PickedFile file) {
|
|
||||||
pickedFiles[name] = file;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
APIRequest addBytesFile(String name, BytesFile file) {
|
APIRequest addBytesFile(String name, BytesFile file) {
|
||||||
this.bytesFiles[name] = file;
|
this.bytesFiles[name] = file;
|
||||||
return this;
|
return this;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:comunic/helpers/database/database_contract.dart';
|
import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
|
||||||
import 'package:comunic/models/cache_model.dart';
|
import 'package:comunic/models/conversation_member.dart';
|
||||||
import 'package:comunic/utils/account_utils.dart';
|
import 'package:comunic/utils/account_utils.dart';
|
||||||
import 'package:comunic/utils/list_utils.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
/// Conversation model
|
/// Conversation model
|
||||||
@ -10,79 +10,100 @@ import 'package:meta/meta.dart';
|
|||||||
|
|
||||||
enum CallCapabilities { NONE, AUDIO, VIDEO }
|
enum CallCapabilities { NONE, AUDIO, VIDEO }
|
||||||
|
|
||||||
class Conversation extends CacheModel implements Comparable {
|
class Conversation extends SerializableElement<Conversation> {
|
||||||
final int ownerID;
|
final int id;
|
||||||
final int lastActive;
|
final int lastActivity;
|
||||||
final String name;
|
final String name;
|
||||||
final bool following;
|
final Color color;
|
||||||
final bool sawLastMessage;
|
final String logoURL;
|
||||||
final List<int> members;
|
final int groupID;
|
||||||
|
final List<ConversationMember> members;
|
||||||
final bool canEveryoneAddMembers;
|
final bool canEveryoneAddMembers;
|
||||||
final CallCapabilities callCapabilities;
|
final CallCapabilities callCapabilities;
|
||||||
final bool isHavingCall;
|
final bool isHavingCall;
|
||||||
|
|
||||||
const Conversation({
|
Conversation({
|
||||||
@required int id,
|
@required this.id,
|
||||||
@required this.ownerID,
|
@required this.lastActivity,
|
||||||
@required this.lastActive,
|
|
||||||
@required this.name,
|
@required this.name,
|
||||||
@required this.following,
|
@required this.color,
|
||||||
@required this.sawLastMessage,
|
@required this.logoURL,
|
||||||
|
@required this.groupID,
|
||||||
@required this.members,
|
@required this.members,
|
||||||
@required this.canEveryoneAddMembers,
|
@required this.canEveryoneAddMembers,
|
||||||
this.callCapabilities = CallCapabilities.NONE,
|
this.callCapabilities = CallCapabilities.NONE,
|
||||||
this.isHavingCall = false,
|
this.isHavingCall = false,
|
||||||
}) : assert(id != null),
|
}) : assert(id != null),
|
||||||
assert(ownerID != null),
|
assert(lastActivity != null),
|
||||||
assert(lastActive != null),
|
|
||||||
assert(following != null),
|
|
||||||
assert(sawLastMessage != null),
|
|
||||||
assert(members != null),
|
assert(members != null),
|
||||||
assert(canEveryoneAddMembers != null),
|
assert(canEveryoneAddMembers != null),
|
||||||
assert(callCapabilities != null),
|
assert(callCapabilities != null),
|
||||||
assert(isHavingCall != null),
|
assert(isHavingCall != null);
|
||||||
super(id: id);
|
|
||||||
|
|
||||||
/// Check out whether a conversation has a fixed name or not
|
/// Check out whether a conversation has a fixed name or not
|
||||||
bool get hasName => this.name != null;
|
bool get hasName => this.name != null;
|
||||||
|
|
||||||
/// Check out whether current user of the application is the owner of it or
|
/// Get current user membership
|
||||||
/// not
|
ConversationMember get membership =>
|
||||||
bool get isOwner => this.ownerID == userID();
|
members.firstWhere((m) => m.userID == userID());
|
||||||
|
|
||||||
Conversation.fromMap(Map<String, dynamic> map)
|
/// Check out whether current user of the application is an admin
|
||||||
: ownerID = map[ConversationTableContract.C_OWNER_ID],
|
bool get isAdmin => membership.isAdmin;
|
||||||
lastActive = map[ConversationTableContract.C_LAST_ACTIVE],
|
|
||||||
name = map[ConversationTableContract.C_NAME],
|
/// Check if current user is the last admin of the conversation
|
||||||
following = map[ConversationTableContract.C_FOLLOWING] == 1,
|
bool get isLastAdmin => isAdmin && adminsID.length == 1;
|
||||||
sawLastMessage = map[ConversationTableContract.C_SAW_LAST_MESSAGE] == 1,
|
|
||||||
members =
|
/// Check it current user is following the conversation or not
|
||||||
listToIntList(map[ConversationTableContract.C_MEMBERS].split(",")),
|
bool get following => membership.following;
|
||||||
canEveryoneAddMembers =
|
|
||||||
map[ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS] == 1,
|
/// Get the list of members in the conversation
|
||||||
|
Set<int> get membersID => members.map((e) => e.userID).toSet();
|
||||||
|
|
||||||
|
/// Get the list of admins in the conversation
|
||||||
|
Set<int> get adminsID =>
|
||||||
|
members.where((e) => e.isAdmin).map((e) => e.userID).toSet();
|
||||||
|
|
||||||
|
/// Get the list of the OTHER members of the conversation (all except current user)
|
||||||
|
Set<int> get otherMembersID => membersID..remove(userID());
|
||||||
|
|
||||||
|
/// Check if the last message has been seen or not
|
||||||
|
bool get sawLastMessage => lastActivity <= membership.lastAccessTime;
|
||||||
|
|
||||||
|
/// Check out whether a conversation is managed or not
|
||||||
|
bool get isManaged => groupID != null;
|
||||||
|
|
||||||
|
Conversation.fromJson(Map<String, dynamic> map)
|
||||||
|
: id = map["id"],
|
||||||
|
name = map["name"],
|
||||||
|
color = map["color"] == null ? null : Color(map["color"]),
|
||||||
|
logoURL = map["logoURL"],
|
||||||
|
groupID = map["groupID"],
|
||||||
|
lastActivity = map["lastActivity"],
|
||||||
|
members = map["members"]
|
||||||
|
.map((el) => ConversationMember.fromJSON(el))
|
||||||
|
.toList()
|
||||||
|
.cast<ConversationMember>(),
|
||||||
|
canEveryoneAddMembers = map["canEveryoneAddMembers"],
|
||||||
|
|
||||||
// By default, we can not do any call
|
// By default, we can not do any call
|
||||||
callCapabilities = CallCapabilities.NONE,
|
callCapabilities = CallCapabilities.NONE,
|
||||||
isHavingCall = false,
|
isHavingCall = false;
|
||||||
super.fromMap(map);
|
|
||||||
|
|
||||||
@override
|
Map<String, dynamic> toJson() {
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
return {
|
||||||
ConversationTableContract.C_ID: id,
|
"id": id,
|
||||||
ConversationTableContract.C_OWNER_ID: ownerID,
|
"name": name,
|
||||||
ConversationTableContract.C_LAST_ACTIVE: lastActive,
|
"color": color?.value,
|
||||||
ConversationTableContract.C_NAME: name,
|
"logoURL": logoURL,
|
||||||
ConversationTableContract.C_FOLLOWING: following ? 1 : 0,
|
"groupID": groupID,
|
||||||
ConversationTableContract.C_SAW_LAST_MESSAGE: sawLastMessage ? 1 : 0,
|
"lastActivity": lastActivity,
|
||||||
ConversationTableContract.C_MEMBERS: members.join(","),
|
"members": members.map((e) => e.toJson()).toList(),
|
||||||
ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS:
|
"canEveryoneAddMembers": canEveryoneAddMembers,
|
||||||
canEveryoneAddMembers ? 1 : 0
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int compareTo(other) {
|
int compareTo(Conversation other) {
|
||||||
return other.lastActive.compareTo(this.lastActive);
|
return other.lastActivity.compareTo(this.lastActivity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
lib/models/conversation_member.dart
Normal file
40
lib/models/conversation_member.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// Conversation member
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class ConversationMember {
|
||||||
|
final int userID;
|
||||||
|
final int lastMessageSeen;
|
||||||
|
final int lastAccessTime;
|
||||||
|
final bool following;
|
||||||
|
final bool isAdmin;
|
||||||
|
|
||||||
|
const ConversationMember({
|
||||||
|
@required this.userID,
|
||||||
|
@required this.lastMessageSeen,
|
||||||
|
@required this.lastAccessTime,
|
||||||
|
@required this.following,
|
||||||
|
@required this.isAdmin,
|
||||||
|
}) : assert(userID != null),
|
||||||
|
assert(lastMessageSeen != null),
|
||||||
|
assert(lastAccessTime != null),
|
||||||
|
assert(following != null),
|
||||||
|
assert(isAdmin != null);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'userID': userID,
|
||||||
|
'lastMessageSeen': lastMessageSeen,
|
||||||
|
'lastAccessTime': lastAccessTime,
|
||||||
|
'following': following,
|
||||||
|
'isAdmin': isAdmin,
|
||||||
|
};
|
||||||
|
|
||||||
|
ConversationMember.fromJSON(Map<String, dynamic> json)
|
||||||
|
: userID = json["userID"],
|
||||||
|
lastMessageSeen = json["lastMessageSeen"],
|
||||||
|
lastAccessTime = json["lastAccessTime"],
|
||||||
|
following = json["following"],
|
||||||
|
isAdmin = json["isAdmin"];
|
||||||
|
}
|
@ -1,66 +1,260 @@
|
|||||||
import 'package:comunic/helpers/database/database_contract.dart';
|
import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
|
||||||
import 'package:comunic/models/cache_model.dart';
|
import 'package:comunic/lists/users_list.dart';
|
||||||
import 'package:comunic/models/displayed_content.dart';
|
import 'package:comunic/models/displayed_content.dart';
|
||||||
import 'package:comunic/utils/account_utils.dart' as account;
|
import 'package:comunic/utils/account_utils.dart' as account;
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
/// Single conversation message
|
/// Single conversation message
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
class ConversationMessage extends CacheModel implements Comparable {
|
enum ConversationMessageFileType {
|
||||||
final int id;
|
IMAGE,
|
||||||
final int conversationID;
|
VIDEO,
|
||||||
|
AUDIO,
|
||||||
|
PDF,
|
||||||
|
ZIP,
|
||||||
|
OTHER,
|
||||||
|
}
|
||||||
|
|
||||||
|
const _ConversationFileMimeTypeMapping = {
|
||||||
|
"image/jpeg": ConversationMessageFileType.IMAGE,
|
||||||
|
"image/png": ConversationMessageFileType.IMAGE,
|
||||||
|
"image/gif": ConversationMessageFileType.IMAGE,
|
||||||
|
"video/mp4": ConversationMessageFileType.VIDEO,
|
||||||
|
"audio/m4a": ConversationMessageFileType.AUDIO,
|
||||||
|
"audio/mpeg": ConversationMessageFileType.AUDIO,
|
||||||
|
"application/pdf": ConversationMessageFileType.PDF,
|
||||||
|
"application/zip": ConversationMessageFileType.ZIP,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ConversationMessageFile {
|
||||||
|
final String url;
|
||||||
|
final int size;
|
||||||
|
final String name;
|
||||||
|
final String thumbnail;
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
const ConversationMessageFile({
|
||||||
|
@required this.url,
|
||||||
|
@required this.size,
|
||||||
|
@required this.name,
|
||||||
|
@required this.thumbnail,
|
||||||
|
@required this.type,
|
||||||
|
}) : assert(url != null),
|
||||||
|
assert(size != null),
|
||||||
|
assert(name != null),
|
||||||
|
assert(type != null);
|
||||||
|
|
||||||
|
/// Get the type of file
|
||||||
|
ConversationMessageFileType get fileType {
|
||||||
|
if (type != null && _ConversationFileMimeTypeMapping.containsKey(type))
|
||||||
|
return _ConversationFileMimeTypeMapping[type];
|
||||||
|
else
|
||||||
|
return ConversationMessageFileType.OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the icon associated with file type
|
||||||
|
IconData get icon {
|
||||||
|
switch (fileType) {
|
||||||
|
case ConversationMessageFileType.IMAGE:
|
||||||
|
return Icons.image;
|
||||||
|
case ConversationMessageFileType.VIDEO:
|
||||||
|
return Icons.video_library;
|
||||||
|
case ConversationMessageFileType.AUDIO:
|
||||||
|
return Icons.audiotrack;
|
||||||
|
case ConversationMessageFileType.PDF:
|
||||||
|
return Icons.picture_as_pdf;
|
||||||
|
|
||||||
|
case ConversationMessageFileType.ZIP:
|
||||||
|
return Icons.archive;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Icons.insert_drive_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasThumbnail => thumbnail != null;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"url": url,
|
||||||
|
"size": size,
|
||||||
|
"name": name,
|
||||||
|
"thumbnail": thumbnail,
|
||||||
|
"type": type
|
||||||
|
};
|
||||||
|
|
||||||
|
ConversationMessageFile.fromJson(Map<String, dynamic> json)
|
||||||
|
: url = json["url"],
|
||||||
|
size = json["size"],
|
||||||
|
name = json["name"],
|
||||||
|
thumbnail = json["thumbnail"],
|
||||||
|
type = json["type"];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConversationServerMessageType {
|
||||||
|
USER_CREATED_CONVERSATION,
|
||||||
|
USER_ADDED_ANOTHER_USER,
|
||||||
|
USER_LEFT_CONV,
|
||||||
|
USER_REMOVED_ANOTHER_USER
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConversationServerMessage {
|
||||||
|
final ConversationServerMessageType type;
|
||||||
final int userID;
|
final int userID;
|
||||||
final int timeInsert;
|
final int userWhoAdded;
|
||||||
final DisplayedString message;
|
final int userAdded;
|
||||||
final String imageURL;
|
final int userWhoRemoved;
|
||||||
|
final int userRemoved;
|
||||||
|
|
||||||
const ConversationMessage({
|
const ConversationServerMessage({
|
||||||
@required this.id,
|
@required this.type,
|
||||||
@required this.conversationID,
|
|
||||||
@required this.userID,
|
@required this.userID,
|
||||||
@required this.timeInsert,
|
@required this.userWhoAdded,
|
||||||
@required this.message,
|
@required this.userAdded,
|
||||||
@required this.imageURL,
|
@required this.userWhoRemoved,
|
||||||
}) : assert(id != null),
|
@required this.userRemoved,
|
||||||
assert(userID != null),
|
}) : assert(type != null),
|
||||||
assert(timeInsert != null),
|
assert(userID != null ||
|
||||||
assert(message != null),
|
(type != ConversationServerMessageType.USER_CREATED_CONVERSATION &&
|
||||||
super(id: id);
|
type != ConversationServerMessageType.USER_LEFT_CONV)),
|
||||||
|
assert((userWhoAdded != null && userAdded != null) ||
|
||||||
|
type != ConversationServerMessageType.USER_ADDED_ANOTHER_USER),
|
||||||
|
assert((userWhoRemoved != null && userRemoved != null) ||
|
||||||
|
type != ConversationServerMessageType.USER_REMOVED_ANOTHER_USER);
|
||||||
|
|
||||||
DateTime get date => DateTime.fromMillisecondsSinceEpoch(timeInsert * 1000);
|
Set<int> get usersID {
|
||||||
|
switch (type) {
|
||||||
|
case ConversationServerMessageType.USER_CREATED_CONVERSATION:
|
||||||
|
case ConversationServerMessageType.USER_LEFT_CONV:
|
||||||
|
return Set()..add(userID);
|
||||||
|
|
||||||
|
case ConversationServerMessageType.USER_ADDED_ANOTHER_USER:
|
||||||
|
return Set()..add(userWhoAdded)..add(userAdded);
|
||||||
|
|
||||||
|
case ConversationServerMessageType.USER_REMOVED_ANOTHER_USER:
|
||||||
|
return Set()..add(userWhoRemoved)..add(userRemoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception("Unsupported server message type!");
|
||||||
|
}
|
||||||
|
|
||||||
|
String getText(UsersList list) {
|
||||||
|
switch (type) {
|
||||||
|
case ConversationServerMessageType.USER_CREATED_CONVERSATION:
|
||||||
|
return tr("%1% created the conversation",
|
||||||
|
args: {"1": list.getUser(userID).fullName});
|
||||||
|
|
||||||
|
case ConversationServerMessageType.USER_ADDED_ANOTHER_USER:
|
||||||
|
return tr("%1% added %2% to the conversation", args: {
|
||||||
|
"1": list.getUser(userWhoAdded).fullName,
|
||||||
|
"2": list.getUser(userAdded).fullName,
|
||||||
|
});
|
||||||
|
|
||||||
|
case ConversationServerMessageType.USER_LEFT_CONV:
|
||||||
|
return tr("%1% left the conversation", args: {
|
||||||
|
"1": list.getUser(userID).fullName,
|
||||||
|
});
|
||||||
|
|
||||||
|
case ConversationServerMessageType.USER_REMOVED_ANOTHER_USER:
|
||||||
|
return tr("%1% removed %2% from the conversation", args: {
|
||||||
|
"1": list.getUser(userWhoRemoved).fullName,
|
||||||
|
"2": list.getUser(userRemoved).fullName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception("Unsupported message type!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"type": type.toString(),
|
||||||
|
"userID": userID,
|
||||||
|
"userWhoAdded": userWhoAdded,
|
||||||
|
"userAdded": userAdded,
|
||||||
|
"userWhoRemoved": userWhoRemoved,
|
||||||
|
"userRemoved": userRemoved,
|
||||||
|
};
|
||||||
|
|
||||||
|
ConversationServerMessage.fromJson(Map<String, dynamic> json)
|
||||||
|
: type = ConversationServerMessageType.values
|
||||||
|
.firstWhere((el) => el.toString() == json["type"]),
|
||||||
|
userID = json["userID"],
|
||||||
|
userWhoAdded = json["userWhoAdded"],
|
||||||
|
userAdded = json["userAdded"],
|
||||||
|
userWhoRemoved = json["userWhoRemoved"],
|
||||||
|
userRemoved = json["userRemoved"];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConversationMessage extends SerializableElement<ConversationMessage> {
|
||||||
|
final int id;
|
||||||
|
final int convID;
|
||||||
|
final int userID;
|
||||||
|
final int timeSent;
|
||||||
|
final DisplayedString message;
|
||||||
|
final ConversationMessageFile file;
|
||||||
|
final ConversationServerMessage serverMessage;
|
||||||
|
|
||||||
|
ConversationMessage({
|
||||||
|
@required this.id,
|
||||||
|
@required this.convID,
|
||||||
|
@required this.userID,
|
||||||
|
@required this.timeSent,
|
||||||
|
@required this.message,
|
||||||
|
@required this.file,
|
||||||
|
@required this.serverMessage,
|
||||||
|
}) : assert(id != null),
|
||||||
|
assert(convID != null),
|
||||||
|
assert(userID != null || serverMessage != null),
|
||||||
|
assert(timeSent != null),
|
||||||
|
assert(message != null || file != null || serverMessage != null);
|
||||||
|
|
||||||
|
DateTime get date => DateTime.fromMillisecondsSinceEpoch(timeSent * 1000);
|
||||||
|
|
||||||
bool get hasMessage => !message.isNull && message.length > 0;
|
bool get hasMessage => !message.isNull && message.length > 0;
|
||||||
|
|
||||||
bool get hasImage => imageURL != null && imageURL != "null";
|
bool get hasFile => file != null;
|
||||||
|
|
||||||
bool get isOwner => account.userID() == userID;
|
bool get isOwner => account.userID() == userID;
|
||||||
|
|
||||||
|
bool get isServerMessage => serverMessage != null;
|
||||||
|
|
||||||
|
/// Get the list of the ID of the users implied in this message
|
||||||
|
Set<int> get usersID {
|
||||||
|
if (userID != null) return Set()..add(userID);
|
||||||
|
|
||||||
|
if (serverMessage != null) return serverMessage.usersID;
|
||||||
|
return Set();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int compareTo(other) {
|
int compareTo(ConversationMessage other) {
|
||||||
return id.compareTo(other.id);
|
return id.compareTo(other.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Map<String, dynamic> toJson() {
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
return {
|
||||||
ConversationsMessagesTableContract.C_ID: id,
|
"id": id,
|
||||||
ConversationsMessagesTableContract.C_CONVERSATION_ID: conversationID,
|
"convID": convID,
|
||||||
ConversationsMessagesTableContract.C_USER_ID: userID,
|
"userID": userID,
|
||||||
ConversationsMessagesTableContract.C_TIME_INSERT: timeInsert,
|
"timeSent": timeSent,
|
||||||
ConversationsMessagesTableContract.C_MESSAGE: message.content,
|
"message": message.content,
|
||||||
ConversationsMessagesTableContract.C_IMAGE_URL: imageURL
|
"file": file?.toJson(),
|
||||||
|
"serverMessage": serverMessage?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ConversationMessage.fromMap(Map<String, dynamic> map)
|
ConversationMessage.fromJson(Map<String, dynamic> map)
|
||||||
: id = map[ConversationsMessagesTableContract.C_ID],
|
: id = map["id"],
|
||||||
conversationID =
|
convID = map["convID"],
|
||||||
map[ConversationsMessagesTableContract.C_CONVERSATION_ID],
|
userID = map["userID"],
|
||||||
userID = map[ConversationsMessagesTableContract.C_USER_ID],
|
timeSent = map["timeSent"],
|
||||||
timeInsert = map[ConversationsMessagesTableContract.C_TIME_INSERT],
|
message = DisplayedString(map["message"]),
|
||||||
message = DisplayedString(map[ConversationsMessagesTableContract.C_MESSAGE]),
|
file = map["file"] == null
|
||||||
imageURL = map[ConversationsMessagesTableContract.C_IMAGE_URL],
|
? null
|
||||||
super.fromMap(map);
|
: ConversationMessageFile.fromJson(map["file"]),
|
||||||
|
serverMessage = map["serverMessage"] == null
|
||||||
|
? null
|
||||||
|
: ConversationServerMessage.fromJson(map["serverMessage"]);
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class Membership {
|
|||||||
case MembershipType.GROUP:
|
case MembershipType.GROUP:
|
||||||
return groupLastActive;
|
return groupLastActive;
|
||||||
case MembershipType.CONVERSATION:
|
case MembershipType.CONVERSATION:
|
||||||
return conversation.lastActive;
|
return conversation.lastActivity;
|
||||||
default:
|
default:
|
||||||
throw Exception("Unreachable statment!");
|
throw Exception("Unreachable statment!");
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import 'api_request.dart';
|
||||||
|
|
||||||
/// New comment information
|
/// New comment information
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
@ -8,7 +9,7 @@ import 'package:meta/meta.dart';
|
|||||||
class NewComment {
|
class NewComment {
|
||||||
final int postID;
|
final int postID;
|
||||||
final String content;
|
final String content;
|
||||||
final PickedFile image;
|
final BytesFile image;
|
||||||
|
|
||||||
const NewComment({
|
const NewComment({
|
||||||
@required this.postID,
|
@required this.postID,
|
||||||
|
24
lib/models/new_conversation.dart
Normal file
24
lib/models/new_conversation.dart
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
/// New conversation information
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class NewConversation {
|
||||||
|
final String name;
|
||||||
|
final List<int> members;
|
||||||
|
final bool follow;
|
||||||
|
final bool canEveryoneAddMembers;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const NewConversation({
|
||||||
|
@required this.name,
|
||||||
|
@required this.members,
|
||||||
|
@required this.follow,
|
||||||
|
@required this.canEveryoneAddMembers,
|
||||||
|
@required this.color,
|
||||||
|
}) : assert(members != null),
|
||||||
|
assert(members.length > 0),
|
||||||
|
assert(follow != null),
|
||||||
|
assert(canEveryoneAddMembers != null);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:comunic/models/api_request.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
/// New conversation message model
|
/// New conversation message model
|
||||||
@ -10,14 +10,20 @@ import 'package:meta/meta.dart';
|
|||||||
class NewConversationMessage {
|
class NewConversationMessage {
|
||||||
final int conversationID;
|
final int conversationID;
|
||||||
final String message;
|
final String message;
|
||||||
final PickedFile image;
|
final BytesFile file;
|
||||||
|
final BytesFile thumbnail;
|
||||||
|
|
||||||
NewConversationMessage(
|
NewConversationMessage({
|
||||||
{@required this.conversationID, @required this.message, this.image})
|
@required this.conversationID,
|
||||||
: assert(conversationID != null),
|
@required this.message,
|
||||||
assert(image != null || message != null);
|
this.file,
|
||||||
|
this.thumbnail,
|
||||||
|
}) : assert(conversationID != null),
|
||||||
|
assert(file != null || message != null);
|
||||||
|
|
||||||
bool get hasMessage => message != null;
|
bool get hasMessage => message != null;
|
||||||
|
|
||||||
bool get hasImage => image != null;
|
bool get hasFile => file != null;
|
||||||
|
|
||||||
|
bool get hasThumbnail => thumbnail != null;
|
||||||
}
|
}
|
||||||
|
27
lib/models/new_conversation_settings.dart
Normal file
27
lib/models/new_conversation_settings.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// Conversation settings update
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class NewConversationsSettings {
|
||||||
|
final int convID;
|
||||||
|
final bool following;
|
||||||
|
final bool isComplete;
|
||||||
|
final String name;
|
||||||
|
final bool canEveryoneAddMembers;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const NewConversationsSettings({
|
||||||
|
@required this.convID,
|
||||||
|
@required this.following,
|
||||||
|
@required this.isComplete,
|
||||||
|
@required this.name,
|
||||||
|
@required this.canEveryoneAddMembers,
|
||||||
|
@required this.color,
|
||||||
|
}) : assert(convID != null),
|
||||||
|
assert(convID > 0),
|
||||||
|
assert(following != null),
|
||||||
|
assert(isComplete != null),
|
||||||
|
assert(!isComplete || canEveryoneAddMembers != null);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
|
import 'api_request.dart';
|
||||||
|
|
||||||
/// New emoji information
|
/// New emoji information
|
||||||
///
|
///
|
||||||
@ -7,7 +8,7 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
|
|
||||||
class NewEmoji {
|
class NewEmoji {
|
||||||
final String shortcut;
|
final String shortcut;
|
||||||
final PickedFile image;
|
final BytesFile image;
|
||||||
|
|
||||||
const NewEmoji({
|
const NewEmoji({
|
||||||
@required this.shortcut,
|
@required this.shortcut,
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:comunic/enums/post_kind.dart';
|
import 'package:comunic/enums/post_kind.dart';
|
||||||
import 'package:comunic/enums/post_target.dart';
|
import 'package:comunic/enums/post_target.dart';
|
||||||
import 'package:comunic/enums/post_visibility_level.dart';
|
import 'package:comunic/enums/post_visibility_level.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import 'api_request.dart';
|
||||||
|
|
||||||
/// New post information
|
/// New post information
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
@ -27,7 +28,7 @@ class NewPost {
|
|||||||
final int targetID;
|
final int targetID;
|
||||||
final PostVisibilityLevel visibility;
|
final PostVisibilityLevel visibility;
|
||||||
final String content;
|
final String content;
|
||||||
final PickedFile image;
|
final BytesFile image;
|
||||||
final String url;
|
final String url;
|
||||||
final List<int> pdf;
|
final List<int> pdf;
|
||||||
final PostKind kind;
|
final PostKind kind;
|
||||||
|
@ -57,6 +57,47 @@ class ServerDataConservationPolicy {
|
|||||||
assert(minLikesLifetime != null);
|
assert(minLikesLifetime != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ConversationsPolicy {
|
||||||
|
final int minMessageLen;
|
||||||
|
final int maxMessageLen;
|
||||||
|
final List<String> allowedFilesType;
|
||||||
|
final int filesMaxSize;
|
||||||
|
final int writingEventInterval;
|
||||||
|
final int writingEventLifetime;
|
||||||
|
final int maxMessageImageWidth;
|
||||||
|
final int maxMessageImageHeight;
|
||||||
|
final int maxThumbnailWidth;
|
||||||
|
final int maxThumbnailHeight;
|
||||||
|
final int maxLogoWidth;
|
||||||
|
final int maxLogoHeight;
|
||||||
|
|
||||||
|
const ConversationsPolicy({
|
||||||
|
@required this.minMessageLen,
|
||||||
|
@required this.maxMessageLen,
|
||||||
|
@required this.allowedFilesType,
|
||||||
|
@required this.filesMaxSize,
|
||||||
|
@required this.writingEventInterval,
|
||||||
|
@required this.writingEventLifetime,
|
||||||
|
@required this.maxMessageImageWidth,
|
||||||
|
@required this.maxMessageImageHeight,
|
||||||
|
@required this.maxThumbnailWidth,
|
||||||
|
@required this.maxThumbnailHeight,
|
||||||
|
@required this.maxLogoWidth,
|
||||||
|
@required this.maxLogoHeight,
|
||||||
|
}) : assert(minMessageLen != null),
|
||||||
|
assert(maxMessageLen != null),
|
||||||
|
assert(allowedFilesType != null),
|
||||||
|
assert(filesMaxSize != null),
|
||||||
|
assert(writingEventInterval != null),
|
||||||
|
assert(writingEventLifetime != null),
|
||||||
|
assert(maxMessageImageWidth != null),
|
||||||
|
assert(maxMessageImageHeight != null),
|
||||||
|
assert(maxThumbnailWidth != null),
|
||||||
|
assert(maxThumbnailHeight != null),
|
||||||
|
assert(maxLogoWidth != null),
|
||||||
|
assert(maxLogoHeight != null);
|
||||||
|
}
|
||||||
|
|
||||||
class ServerConfig {
|
class ServerConfig {
|
||||||
final Version minSupportedMobileVersion;
|
final Version minSupportedMobileVersion;
|
||||||
final String termsURL;
|
final String termsURL;
|
||||||
@ -64,6 +105,7 @@ class ServerConfig {
|
|||||||
final String androidDirectDownloadURL;
|
final String androidDirectDownloadURL;
|
||||||
final PasswordPolicy passwordPolicy;
|
final PasswordPolicy passwordPolicy;
|
||||||
final ServerDataConservationPolicy dataConservationPolicy;
|
final ServerDataConservationPolicy dataConservationPolicy;
|
||||||
|
final ConversationsPolicy conversationsPolicy;
|
||||||
|
|
||||||
const ServerConfig({
|
const ServerConfig({
|
||||||
@required this.minSupportedMobileVersion,
|
@required this.minSupportedMobileVersion,
|
||||||
@ -72,10 +114,12 @@ class ServerConfig {
|
|||||||
@required this.androidDirectDownloadURL,
|
@required this.androidDirectDownloadURL,
|
||||||
@required this.passwordPolicy,
|
@required this.passwordPolicy,
|
||||||
@required this.dataConservationPolicy,
|
@required this.dataConservationPolicy,
|
||||||
|
@required this.conversationsPolicy,
|
||||||
}) : assert(minSupportedMobileVersion != null),
|
}) : assert(minSupportedMobileVersion != null),
|
||||||
assert(termsURL != null),
|
assert(termsURL != null),
|
||||||
assert(playStoreURL != null),
|
assert(playStoreURL != null),
|
||||||
assert(androidDirectDownloadURL != null),
|
assert(androidDirectDownloadURL != null),
|
||||||
assert(passwordPolicy != null),
|
assert(passwordPolicy != null),
|
||||||
assert(dataConservationPolicy != null);
|
assert(dataConservationPolicy != null),
|
||||||
|
assert(conversationsPolicy != null);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Unread conversation information
|
/// Unread conversation information
|
||||||
@ -5,21 +7,12 @@ import 'package:flutter/material.dart';
|
|||||||
/// @author Pierre Hubert
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
class UnreadConversation {
|
class UnreadConversation {
|
||||||
final int id;
|
final Conversation conv;
|
||||||
final String convName;
|
final ConversationMessage message;
|
||||||
final int lastActive;
|
|
||||||
final int userID;
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
const UnreadConversation({
|
const UnreadConversation({
|
||||||
@required this.id,
|
@required this.conv,
|
||||||
@required this.convName,
|
|
||||||
@required this.lastActive,
|
|
||||||
@required this.userID,
|
|
||||||
@required this.message,
|
@required this.message,
|
||||||
}) : assert(id != null),
|
}) : assert(conv != null),
|
||||||
assert(convName != null),
|
|
||||||
assert(lastActive != null),
|
|
||||||
assert(userID != null),
|
|
||||||
assert(message != null);
|
assert(message != null);
|
||||||
}
|
}
|
||||||
|
30
lib/ui/dialogs/alert_dialog.dart
Normal file
30
lib/ui/dialogs/alert_dialog.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Simple alert dialog
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
Future<void> alert(BuildContext context, String msg) async {
|
||||||
|
await showDialog(context: context, builder: (c) => _AlertDialog(msg: msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlertDialog extends StatelessWidget {
|
||||||
|
final String msg;
|
||||||
|
|
||||||
|
const _AlertDialog({Key key, @required this.msg})
|
||||||
|
: assert(msg != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: Text(msg),
|
||||||
|
actions: <Widget>[
|
||||||
|
MaterialButton(
|
||||||
|
child: Text("OK"),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
82
lib/ui/dialogs/audio_player_dialog.dart
Normal file
82
lib/ui/dialogs/audio_player_dialog.dart
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import 'package:chewie_audio/chewie_audio.dart';
|
||||||
|
import 'package:comunic/ui/widgets/async_screen_widget.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
/// Audio player dialog
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
/// Show audio player dialog
|
||||||
|
Future<void> showAudioPlayerDialog(BuildContext context, String url) async {
|
||||||
|
showDialog(context: context, builder: (c) => _AudioPlayerDialog(url: url));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioPlayerDialog extends StatefulWidget {
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
const _AudioPlayerDialog({
|
||||||
|
Key key,
|
||||||
|
@required this.url,
|
||||||
|
}) : assert(url != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
__AudioPlayerDialogState createState() => __AudioPlayerDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __AudioPlayerDialogState extends State<_AudioPlayerDialog> {
|
||||||
|
VideoPlayerController _videoPlayerController;
|
||||||
|
ChewieAudioController _chewieAudioController;
|
||||||
|
|
||||||
|
Future<void> _initialize() async {
|
||||||
|
_videoPlayerController = VideoPlayerController.network(widget.url);
|
||||||
|
|
||||||
|
await _videoPlayerController.initialize();
|
||||||
|
|
||||||
|
_chewieAudioController = ChewieAudioController(
|
||||||
|
videoPlayerController: _videoPlayerController,
|
||||||
|
autoPlay: true,
|
||||||
|
looping: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _closeDialog() {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_videoPlayerController != null) _videoPlayerController.dispose();
|
||||||
|
if (_chewieAudioController != null) _chewieAudioController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(tr("Audio Player")),
|
||||||
|
content: _buildContent(),
|
||||||
|
actions: [
|
||||||
|
MaterialButton(
|
||||||
|
onPressed: _closeDialog,
|
||||||
|
child: Text(tr("Close").toUpperCase()),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() => ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: 50, maxWidth: 500),
|
||||||
|
child: AsyncScreenWidget(
|
||||||
|
onReload: _initialize,
|
||||||
|
onBuild: _buildReadyContent,
|
||||||
|
errorMessage: tr("Failed to initialize audio player!"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildReadyContent() =>
|
||||||
|
ChewieAudio(controller: _chewieAudioController);
|
||||||
|
}
|
54
lib/ui/dialogs/color_picker_dialog.dart
Normal file
54
lib/ui/dialogs/color_picker_dialog.dart
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
|
|
||||||
|
/// Color picker dialog
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
Future<Color> showColorPickerDialog(
|
||||||
|
BuildContext context, Color initialColor) async =>
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => _ColorPickerDialog(initialColor: initialColor),
|
||||||
|
);
|
||||||
|
|
||||||
|
class _ColorPickerDialog extends StatefulWidget {
|
||||||
|
final Color initialColor;
|
||||||
|
|
||||||
|
const _ColorPickerDialog({Key key, @required this.initialColor})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
__ColorPickerDialogState createState() => __ColorPickerDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __ColorPickerDialogState extends State<_ColorPickerDialog> {
|
||||||
|
Color _newColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_newColor = widget.initialColor;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: MaterialPicker(
|
||||||
|
pickerColor: _newColor ?? Colors.blue.shade900,
|
||||||
|
onColorChanged: (c) => setState(() => _newColor = c),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
MaterialButton(
|
||||||
|
onPressed: () => Navigator.pop(context, widget.initialColor),
|
||||||
|
child: Text(tr("Cancel").toUpperCase()),
|
||||||
|
),
|
||||||
|
MaterialButton(
|
||||||
|
onPressed: () => Navigator.pop(context, _newColor),
|
||||||
|
child: Text(tr("Ok").toUpperCase()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
220
lib/ui/dialogs/pick_file_dialog.dart
Normal file
220
lib/ui/dialogs/pick_file_dialog.dart
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/ui/dialogs/record_audio_dialog.dart';
|
||||||
|
import 'package:comunic/ui/routes/image_editor_route.dart';
|
||||||
|
import 'package:comunic/utils/files_utils.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:filesize/filesize.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
|
/// Pick file dialog
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
enum _FileChoices {
|
||||||
|
PICK_IMAGE,
|
||||||
|
TAKE_PICTURE,
|
||||||
|
PICK_VIDEO,
|
||||||
|
TAKE_VIDEO,
|
||||||
|
RECORD_AUDIO,
|
||||||
|
PICK_OTHER_FILE,
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef _CanEnable = bool Function(List<String>);
|
||||||
|
typedef _OnOptionSelected = void Function(_FileChoices);
|
||||||
|
|
||||||
|
class _PickFileOption {
|
||||||
|
final _FileChoices value;
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final _CanEnable canEnable;
|
||||||
|
|
||||||
|
const _PickFileOption({
|
||||||
|
@required this.value,
|
||||||
|
@required this.label,
|
||||||
|
@required this.icon,
|
||||||
|
@required this.canEnable,
|
||||||
|
}) : assert(value != null),
|
||||||
|
assert(label != null),
|
||||||
|
assert(icon != null),
|
||||||
|
assert(canEnable != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_PickFileOption> get _optionsList => [
|
||||||
|
// Image
|
||||||
|
_PickFileOption(
|
||||||
|
value: _FileChoices.PICK_IMAGE,
|
||||||
|
label: tr("Choose an image"),
|
||||||
|
icon: Icons.image,
|
||||||
|
canEnable: (l) => l.any(isImage)),
|
||||||
|
_PickFileOption(
|
||||||
|
value: _FileChoices.TAKE_PICTURE,
|
||||||
|
label: tr("Take a picture"),
|
||||||
|
icon: Icons.camera_alt,
|
||||||
|
canEnable: (l) => l.any(isImage)),
|
||||||
|
|
||||||
|
// Video
|
||||||
|
_PickFileOption(
|
||||||
|
value: _FileChoices.PICK_VIDEO,
|
||||||
|
label: tr("Choose a video"),
|
||||||
|
icon: Icons.video_library,
|
||||||
|
canEnable: (l) => l.any(isVideo)),
|
||||||
|
_PickFileOption(
|
||||||
|
value: _FileChoices.TAKE_VIDEO,
|
||||||
|
label: tr("Take a video"),
|
||||||
|
icon: Icons.videocam,
|
||||||
|
canEnable: (l) => l.any(isVideo)),
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
_PickFileOption(
|
||||||
|
value: _FileChoices.RECORD_AUDIO,
|
||||||
|
label: tr("Record audio"),
|
||||||
|
icon: Icons.mic,
|
||||||
|
canEnable: (l) => l.any(isAudio)),
|
||||||
|
|
||||||
|
// Other
|
||||||
|
_PickFileOption(
|
||||||
|
value: _FileChoices.PICK_OTHER_FILE,
|
||||||
|
label: tr("Browse files"),
|
||||||
|
icon: Icons.folder_open,
|
||||||
|
canEnable: (l) =>
|
||||||
|
l.any((el) => !isImage(el) && !isVideo(el) && !isAudio(el))),
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<BytesFile> showPickFileDialog({
|
||||||
|
@required BuildContext context,
|
||||||
|
int maxFileSize,
|
||||||
|
List<String> allowedMimeTypes,
|
||||||
|
int imageMaxWidth,
|
||||||
|
int imageMaxHeight,
|
||||||
|
CropAspectRatio aspectRatio,
|
||||||
|
}) async {
|
||||||
|
assert(allowedMimeTypes != null);
|
||||||
|
|
||||||
|
// Get the list of allowed extension
|
||||||
|
final allowedExtensions = <String>[];
|
||||||
|
for (var mime in allowedExtensions) {
|
||||||
|
final ext = extensionFromMime(mime);
|
||||||
|
if (ext != mime) allowedExtensions.add(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display bottom sheet
|
||||||
|
final choice = await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => BottomSheet(
|
||||||
|
onClosing: () {},
|
||||||
|
builder: (c) => _BottomSheetPickOption(
|
||||||
|
options: _optionsList
|
||||||
|
.where((element) => element.canEnable(allowedMimeTypes))
|
||||||
|
.toList(),
|
||||||
|
onOptionSelected: (v) => Navigator.pop(c, v),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
if (choice == null) return null;
|
||||||
|
|
||||||
|
BytesFile file;
|
||||||
|
switch (choice) {
|
||||||
|
// Pick an image
|
||||||
|
case _FileChoices.PICK_IMAGE:
|
||||||
|
case _FileChoices.TAKE_PICTURE:
|
||||||
|
final image = await ImagePicker().getImage(
|
||||||
|
source: choice == _FileChoices.PICK_IMAGE
|
||||||
|
? ImageSource.gallery
|
||||||
|
: ImageSource.camera,
|
||||||
|
maxWidth: imageMaxWidth.toDouble(),
|
||||||
|
maxHeight: imageMaxHeight.toDouble(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (image == null) return null;
|
||||||
|
|
||||||
|
file = BytesFile(image.path.split("/").last, await image.readAsBytes());
|
||||||
|
|
||||||
|
file = await showImageCropper(context, file, aspectRatio: aspectRatio);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Pick an video
|
||||||
|
case _FileChoices.PICK_VIDEO:
|
||||||
|
case _FileChoices.TAKE_VIDEO:
|
||||||
|
final image = await ImagePicker().getVideo(
|
||||||
|
source: choice == _FileChoices.PICK_VIDEO
|
||||||
|
? ImageSource.gallery
|
||||||
|
: ImageSource.camera,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (image == null) return null;
|
||||||
|
|
||||||
|
file = BytesFile(image.path.split("/").last, await image.readAsBytes());
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Record audio file
|
||||||
|
case _FileChoices.RECORD_AUDIO:
|
||||||
|
final bytes = await showRecordAudioDialog(context);
|
||||||
|
if (bytes == null) return null;
|
||||||
|
file = BytesFile("record.mp3", bytes);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Pick other files
|
||||||
|
case _FileChoices.PICK_OTHER_FILE:
|
||||||
|
final pickedFile = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.any,
|
||||||
|
allowedExtensions: allowedExtensions,
|
||||||
|
allowMultiple: false,
|
||||||
|
withData: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pickedFile == null || pickedFile.files.length == 0) return null;
|
||||||
|
|
||||||
|
file = BytesFile(pickedFile.files[0].name, pickedFile.files[0].bytes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null) return null;
|
||||||
|
|
||||||
|
// Check file size
|
||||||
|
if (maxFileSize != null && file.bytes.length > maxFileSize) {
|
||||||
|
showSimpleSnack(
|
||||||
|
context,
|
||||||
|
tr("This file could not be sent: it is too big! (Max allowed size: %1%)",
|
||||||
|
args: {"1": filesize(file.bytes.length)}));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomSheetPickOption extends StatelessWidget {
|
||||||
|
final List<_PickFileOption> options;
|
||||||
|
final _OnOptionSelected onOptionSelected;
|
||||||
|
|
||||||
|
const _BottomSheetPickOption(
|
||||||
|
{Key key, @required this.options, @required this.onOptionSelected})
|
||||||
|
: assert(options != null),
|
||||||
|
assert(onOptionSelected != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
height: 255,
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 400),
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: options.length,
|
||||||
|
itemBuilder: (c, i) => ListTile(
|
||||||
|
leading: Icon(options[i].icon),
|
||||||
|
title: Text(options[i].label),
|
||||||
|
onTap: () => onOptionSelected(options[i].value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
302
lib/ui/dialogs/record_audio_dialog.dart
Normal file
302
lib/ui/dialogs/record_audio_dialog.dart
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:comunic/ui/dialogs/alert_dialog.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:comunic/utils/log_utils.dart';
|
||||||
|
import 'package:comunic/utils/permission_utils.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:record_mp3/record_mp3.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
/// Record audio dialog
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
/// Record audio
|
||||||
|
Future<Uint8List> showRecordAudioDialog(BuildContext context) async {
|
||||||
|
// Request record permission
|
||||||
|
if (!await requestPermission(context, Permission.microphone)) {
|
||||||
|
alert(context, tr("Did not get permission to access microphone!"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => Scaffold(
|
||||||
|
body: _RecordAudioDialog(),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecordAudioDialog extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
__RecordAudioDialogState createState() => __RecordAudioDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __RecordAudioDialogState extends State<_RecordAudioDialog> {
|
||||||
|
String _recordPath;
|
||||||
|
|
||||||
|
File get _recordFile => _recordPath == null ? null : File(_recordPath);
|
||||||
|
|
||||||
|
bool _recording = false;
|
||||||
|
|
||||||
|
bool get _hasRecord => !_recording && _recordPath != null;
|
||||||
|
|
||||||
|
VideoPlayerController _videoPlayerController;
|
||||||
|
|
||||||
|
bool _playing = false;
|
||||||
|
|
||||||
|
bool _paused = false;
|
||||||
|
|
||||||
|
/// Get record data. This getter can be accessed only once
|
||||||
|
Uint8List get _bytes {
|
||||||
|
final bytes = _recordFile.readAsBytesSync();
|
||||||
|
File(_recordPath).deleteSync();
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposePlayer();
|
||||||
|
RecordMp3.instance.stop();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _disposePlayer() {
|
||||||
|
if (_videoPlayerController != null) {
|
||||||
|
_videoPlayerController.dispose();
|
||||||
|
}
|
||||||
|
_videoPlayerController = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(tr("Audio record")),
|
||||||
|
content: _buildContent(),
|
||||||
|
actions: <Widget>[
|
||||||
|
_ActionButton(
|
||||||
|
visible: !_recording,
|
||||||
|
text: tr("Cancel"),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
_ActionButton(
|
||||||
|
visible: _hasRecord,
|
||||||
|
text: tr("Send"),
|
||||||
|
onPressed: () => Navigator.of(context).pop(_bytes),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _status {
|
||||||
|
if (_recording)
|
||||||
|
return tr("Recording...");
|
||||||
|
else if (_paused)
|
||||||
|
return tr("Playback paused...");
|
||||||
|
else if (_playing)
|
||||||
|
return tr("Playing...");
|
||||||
|
else if (!_hasRecord)
|
||||||
|
return tr("Ready");
|
||||||
|
else
|
||||||
|
return tr("Done");
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() => Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(_status),
|
||||||
|
|
||||||
|
Spacer(),
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
_RecordAction(
|
||||||
|
visible: !_recording && !_playing,
|
||||||
|
icon: Icons.fiber_manual_record,
|
||||||
|
onTap: _startRecording,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Stop recording
|
||||||
|
_RecordAction(
|
||||||
|
visible: _recording,
|
||||||
|
icon: Icons.stop,
|
||||||
|
onTap: _stopRecording,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Play recording
|
||||||
|
_RecordAction(
|
||||||
|
visible: !_recording && _hasRecord && !_playing,
|
||||||
|
icon: Icons.play_arrow,
|
||||||
|
onTap: _playRecord,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Pause playback
|
||||||
|
_RecordAction(
|
||||||
|
visible: _playing && !_paused,
|
||||||
|
icon: Icons.pause,
|
||||||
|
onTap: _pausePlayback,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Resume recording
|
||||||
|
_RecordAction(
|
||||||
|
visible: _paused,
|
||||||
|
icon: Icons.play_arrow,
|
||||||
|
onTap: _resumePlayback,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Stop recording
|
||||||
|
_RecordAction(
|
||||||
|
visible: _playing,
|
||||||
|
icon: Icons.stop,
|
||||||
|
onTap: _stopPlayback,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
void _startRecording() async {
|
||||||
|
try {
|
||||||
|
if (_recordFile != null) _recordFile.deleteSync();
|
||||||
|
|
||||||
|
final dir = await getTemporaryDirectory();
|
||||||
|
|
||||||
|
_recordPath = path.join(dir.absolute.path, "tmp-audio-record.mp3");
|
||||||
|
|
||||||
|
RecordMp3.instance.start(_recordPath, (fail) {
|
||||||
|
print(fail);
|
||||||
|
snack(context, tr("Failed to start recording!"));
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() => _recording = true);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Error while recording!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopRecording() async {
|
||||||
|
try {
|
||||||
|
RecordMp3.instance.stop();
|
||||||
|
|
||||||
|
setState(() => _recording = false);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Error while recording!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _playRecord() async {
|
||||||
|
try {
|
||||||
|
_disposePlayer();
|
||||||
|
|
||||||
|
_videoPlayerController = VideoPlayerController.file(File(_recordPath));
|
||||||
|
await _videoPlayerController.initialize();
|
||||||
|
|
||||||
|
_videoPlayerController.addListener(() async {
|
||||||
|
if (_videoPlayerController == null) return;
|
||||||
|
|
||||||
|
if (_videoPlayerController.value.position ==
|
||||||
|
_videoPlayerController.value.duration) _stopPlayback();
|
||||||
|
});
|
||||||
|
|
||||||
|
await _videoPlayerController.play();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_playing = true;
|
||||||
|
_paused = false;
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Error while playing record!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pausePlayback() async {
|
||||||
|
try {
|
||||||
|
await _videoPlayerController.pause();
|
||||||
|
setState(() => _paused = true);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Error while pausing playback!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resumePlayback() async {
|
||||||
|
try {
|
||||||
|
await _videoPlayerController.play();
|
||||||
|
setState(() => _paused = false);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Error while resuming playback!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopPlayback() async {
|
||||||
|
try {
|
||||||
|
_disposePlayer();
|
||||||
|
setState(() {
|
||||||
|
_paused = false;
|
||||||
|
_playing = false;
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Error while stopping playback!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecordAction extends StatelessWidget {
|
||||||
|
final bool visible;
|
||||||
|
final IconData icon;
|
||||||
|
final void Function() onTap;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const _RecordAction({
|
||||||
|
Key key,
|
||||||
|
@required this.visible,
|
||||||
|
@required this.icon,
|
||||||
|
@required this.onTap,
|
||||||
|
this.color,
|
||||||
|
}) : assert(visible != null),
|
||||||
|
assert(icon != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!visible) return Container(width: 0, height: 0);
|
||||||
|
return IconButton(icon: Icon(icon, color: color), onPressed: onTap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionButton extends StatelessWidget {
|
||||||
|
final bool visible;
|
||||||
|
final String text;
|
||||||
|
final void Function() onPressed;
|
||||||
|
|
||||||
|
const _ActionButton({
|
||||||
|
Key key,
|
||||||
|
this.visible,
|
||||||
|
this.text,
|
||||||
|
this.onPressed,
|
||||||
|
}) : assert(visible != null),
|
||||||
|
assert(text != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!visible) return Container();
|
||||||
|
return MaterialButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Text(text.toUpperCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
95
lib/ui/routes/conversation_message_stats_route.dart
Normal file
95
lib/ui/routes/conversation_message_stats_route.dart
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import 'package:comunic/helpers/users_helper.dart';
|
||||||
|
import 'package:comunic/lists/users_list.dart';
|
||||||
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||||
|
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/async_screen_widget.dart';
|
||||||
|
import 'package:comunic/utils/date_utils.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Conversation message statistics route
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class ConversationMessageStatsRoute extends StatefulWidget {
|
||||||
|
final Conversation conv;
|
||||||
|
final ConversationMessage message;
|
||||||
|
|
||||||
|
const ConversationMessageStatsRoute({
|
||||||
|
Key key,
|
||||||
|
@required this.conv,
|
||||||
|
@required this.message,
|
||||||
|
}) : assert(conv != null),
|
||||||
|
assert(message != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ConversationMessageStatsRouteState createState() =>
|
||||||
|
_ConversationMessageStatsRouteState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConversationMessageStatsRouteState
|
||||||
|
extends State<ConversationMessageStatsRoute> {
|
||||||
|
UsersList _users;
|
||||||
|
|
||||||
|
Future<void> _init() async {
|
||||||
|
_users = await UsersHelper()
|
||||||
|
.getList(widget.conv.membersID..add(widget.message.userID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(tr("Message statistics")),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
onPressed: () => MainController.of(context).popPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: AsyncScreenWidget(
|
||||||
|
onReload: _init,
|
||||||
|
onBuild: _buildScreen,
|
||||||
|
errorMessage: tr("Failed to load message information!")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> get _firstItems => [
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.access_time_rounded),
|
||||||
|
title: Text(tr("Created on")),
|
||||||
|
subtitle: Text(dateTimeToString(widget.message.date)),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: AccountImageWidget(
|
||||||
|
user: _users.getUser(widget.message.userID),
|
||||||
|
),
|
||||||
|
title: Text(_users.getUser(widget.message.userID).fullName),
|
||||||
|
subtitle: Text(tr("Creator")),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget _buildScreen() => ListView.builder(
|
||||||
|
itemCount: _firstItems.length + widget.conv.members.length,
|
||||||
|
itemBuilder: (c, i) {
|
||||||
|
final firstItems = _firstItems;
|
||||||
|
if (i < firstItems.length) return firstItems[i];
|
||||||
|
|
||||||
|
final convMember = widget.conv.members[i - firstItems.length];
|
||||||
|
|
||||||
|
if (convMember.userID == widget.message.userID) return Container();
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: AccountImageWidget(
|
||||||
|
user: _users.getUser(convMember.userID),
|
||||||
|
),
|
||||||
|
title: Text(_users.getUser(convMember.userID).fullName),
|
||||||
|
subtitle: Text(convMember.lastMessageSeen < widget.message.id
|
||||||
|
? tr("Message not seen yet")
|
||||||
|
: tr("Message seen")),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -1,9 +1,13 @@
|
|||||||
import 'package:comunic/helpers/conversations_helper.dart';
|
import 'package:comunic/helpers/conversations_helper.dart';
|
||||||
|
import 'package:comunic/helpers/users_helper.dart';
|
||||||
|
import 'package:comunic/lists/users_list.dart';
|
||||||
import 'package:comunic/models/conversation.dart';
|
import 'package:comunic/models/conversation.dart';
|
||||||
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||||
import 'package:comunic/ui/routes/update_conversation_route.dart';
|
import 'package:comunic/ui/routes/update_conversation_route.dart';
|
||||||
import 'package:comunic/ui/screens/conversation_screen.dart';
|
import 'package:comunic/ui/screens/conversation_screen.dart';
|
||||||
import 'package:comunic/ui/widgets/comunic_back_button_widget.dart';
|
import 'package:comunic/ui/widgets/comunic_back_button_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/conversation_image_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/safe_state.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -25,9 +29,10 @@ class ConversationRoute extends StatefulWidget {
|
|||||||
State<StatefulWidget> createState() => _ConversationRouteState();
|
State<StatefulWidget> createState() => _ConversationRouteState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ConversationRouteState extends State<ConversationRoute> {
|
class _ConversationRouteState extends SafeState<ConversationRoute> {
|
||||||
final ConversationsHelper _conversationsHelper = ConversationsHelper();
|
final ConversationsHelper _conversationsHelper = ConversationsHelper();
|
||||||
Conversation _conversation;
|
Conversation _conversation;
|
||||||
|
UsersList _users;
|
||||||
String _conversationName;
|
String _conversationName;
|
||||||
bool _error = false;
|
bool _error = false;
|
||||||
|
|
||||||
@ -42,21 +47,22 @@ class _ConversationRouteState extends State<ConversationRoute> {
|
|||||||
Future<void> _loadConversation() async {
|
Future<void> _loadConversation() async {
|
||||||
setError(false);
|
setError(false);
|
||||||
|
|
||||||
_conversation = await _conversationsHelper.getSingle(widget.conversationID,
|
try {
|
||||||
force: true);
|
_conversation = await _conversationsHelper
|
||||||
|
.getSingle(widget.conversationID, force: true);
|
||||||
|
|
||||||
if (_conversation == null) return setError(true);
|
_users = await UsersHelper().getList(_conversation.membersID);
|
||||||
|
|
||||||
final conversationName =
|
final conversationName =
|
||||||
await ConversationsHelper.getConversationNameAsync(_conversation);
|
ConversationsHelper.getConversationName(_conversation, _users);
|
||||||
|
|
||||||
if (!this.mounted) return null;
|
if (!this.mounted) return null;
|
||||||
|
|
||||||
if (conversationName == null) return setError(true);
|
setState(() => _conversationName = conversationName);
|
||||||
|
} catch (e, s) {
|
||||||
setState(() {
|
print("Failed to get conversation name! $e => $s");
|
||||||
_conversationName = conversationName;
|
setError(true);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openSettings() {
|
void _openSettings() {
|
||||||
@ -73,7 +79,7 @@ class _ConversationRouteState extends State<ConversationRoute> {
|
|||||||
return buildErrorCard(
|
return buildErrorCard(
|
||||||
tr("Could not get conversation information!"),
|
tr("Could not get conversation information!"),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
onPressed: _loadConversation,
|
onPressed: _loadConversation,
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Try again").toUpperCase(),
|
tr("Try again").toUpperCase(),
|
||||||
@ -97,7 +103,12 @@ class _ConversationRouteState extends State<ConversationRoute> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: isTablet(context) ? null : ComunicBackButton(),
|
leading: isTablet(context)
|
||||||
|
? (_conversation == null || _users == null
|
||||||
|
? null
|
||||||
|
: ConversationImageWidget(
|
||||||
|
conversation: _conversation, users: _users))
|
||||||
|
: ComunicBackButton(),
|
||||||
title: Text(
|
title: Text(
|
||||||
_conversationName == null ? tr("Loading") : _conversationName,
|
_conversationName == null ? tr("Loading") : _conversationName,
|
||||||
),
|
),
|
||||||
|
@ -174,9 +174,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
|
|||||||
|
|
||||||
// Submit button
|
// Submit button
|
||||||
Center(
|
Center(
|
||||||
child: RaisedButton(
|
child: ElevatedButton(
|
||||||
color: Colors.blue,
|
|
||||||
textColor: Colors.white,
|
|
||||||
onPressed: _submitForm,
|
onPressed: _submitForm,
|
||||||
child: Text("Submit"),
|
child: Text("Submit"),
|
||||||
),
|
),
|
||||||
|
@ -61,7 +61,7 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> {
|
|||||||
|
|
||||||
/// Step 3b - Answer security questions
|
/// Step 3b - Answer security questions
|
||||||
List<String> _questions;
|
List<String> _questions;
|
||||||
var _questionsControllers = List<TextEditingController>();
|
var _questionsControllers = <TextEditingController>[];
|
||||||
|
|
||||||
List<String> get _answers =>
|
List<String> get _answers =>
|
||||||
_questionsControllers.map((f) => f.text).toList();
|
_questionsControllers.map((f) => f.text).toList();
|
||||||
@ -146,14 +146,14 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(tr("Here are your options to reset your account:")),
|
Text(tr("Here are your options to reset your account:")),
|
||||||
_Spacer(),
|
_Spacer(),
|
||||||
OutlineButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _openSendEmailDialog,
|
onPressed: _openSendEmailDialog,
|
||||||
icon: Icon(Icons.email),
|
icon: Icon(Icons.email),
|
||||||
label: Text(tr("Send us an email to ask for help")),
|
label: Text(tr("Send us an email to ask for help")),
|
||||||
),
|
),
|
||||||
_Spacer(visible: _hasSecurityQuestions),
|
_Spacer(visible: _hasSecurityQuestions),
|
||||||
_hasSecurityQuestions
|
_hasSecurityQuestions
|
||||||
? OutlineButton.icon(
|
? OutlinedButton.icon(
|
||||||
onPressed: _loadSecurityQuestions,
|
onPressed: _loadSecurityQuestions,
|
||||||
icon: Icon(Icons.help_outline),
|
icon: Icon(Icons.help_outline),
|
||||||
label: Text(tr("Answer your security questions")),
|
label: Text(tr("Answer your security questions")),
|
||||||
@ -199,7 +199,7 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> {
|
|||||||
..add(_Spacer())
|
..add(_Spacer())
|
||||||
..addAll(List.generate(_questions.length, _buildSecurityQuestionField))
|
..addAll(List.generate(_questions.length, _buildSecurityQuestionField))
|
||||||
..add(_Spacer())
|
..add(_Spacer())
|
||||||
..add(OutlineButton(
|
..add(OutlinedButton(
|
||||||
onPressed: _canSubmitAnswers ? _submitSecurityAnswers : null,
|
onPressed: _canSubmitAnswers ? _submitSecurityAnswers : null,
|
||||||
child: Text(tr("Submit")),
|
child: Text(tr("Submit")),
|
||||||
)),
|
)),
|
||||||
|
@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
/// Full screen image details
|
/// Full screen image details
|
||||||
///
|
///
|
||||||
@ -22,6 +23,10 @@ class _FullScreenImageRouteState extends State<FullScreenImageRoute> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(tr("Image")),
|
title: Text(tr("Image")),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.launch), onPressed: () => launch(widget.url))
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: PhotoView(imageProvider: CachedNetworkImageProvider(widget.url)),
|
body: PhotoView(imageProvider: CachedNetworkImageProvider(widget.url)),
|
||||||
);
|
);
|
||||||
|
54
lib/ui/routes/image_editor_route.dart
Normal file
54
lib/ui/routes/image_editor_route.dart
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:comunic/utils/log_utils.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
|
|
||||||
|
import '../../models/api_request.dart';
|
||||||
|
import '../../utils/files_utils.dart';
|
||||||
|
|
||||||
|
/// Image cropper route
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
/// Attempt to crop image
|
||||||
|
///
|
||||||
|
/// Return original image in case of error / if the user did not crop the image
|
||||||
|
Future<BytesFile> showImageCropper(BuildContext context, BytesFile source,
|
||||||
|
{CropAspectRatio aspectRatio}) async {
|
||||||
|
assert(context != null);
|
||||||
|
assert(source != null);
|
||||||
|
|
||||||
|
File file;
|
||||||
|
File cropped;
|
||||||
|
|
||||||
|
try {
|
||||||
|
file = await generateTemporaryFile();
|
||||||
|
await file.writeAsBytes(source.bytes);
|
||||||
|
|
||||||
|
File cropped = await ImageCropper.cropImage(
|
||||||
|
sourcePath: file.absolute.path,
|
||||||
|
compressFormat: ImageCompressFormat.png,
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
|
androidUiSettings: AndroidUiSettings(
|
||||||
|
toolbarColor: Colors.black,
|
||||||
|
toolbarTitle: tr("Crop Photo"),
|
||||||
|
toolbarWidgetColor: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cropped == null) return null;
|
||||||
|
|
||||||
|
return BytesFile("cropped.png", await cropped.readAsBytes());
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to execute image cropper!"));
|
||||||
|
return source;
|
||||||
|
} finally {
|
||||||
|
if (file != null && await file.exists()) file.delete();
|
||||||
|
if (cropped != null && await cropped.exists()) cropped.delete();
|
||||||
|
}
|
||||||
|
}
|
@ -127,7 +127,7 @@ class _LoginRouteState extends State<LoginRoute> {
|
|||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: _loading
|
child: _loading
|
||||||
? CircularProgressIndicator()
|
? CircularProgressIndicator()
|
||||||
: RaisedButton(
|
: ElevatedButton(
|
||||||
child: Text(tr("Sign in")),
|
child: Text(tr("Sign in")),
|
||||||
onPressed: valid ? () => _submitForm(context) : null,
|
onPressed: valid ? () => _submitForm(context) : null,
|
||||||
),
|
),
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import 'package:comunic/helpers/account_helper.dart';
|
import 'package:comunic/helpers/account_helper.dart';
|
||||||
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
import 'package:comunic/ui/routes/conversation_message_stats_route.dart';
|
||||||
import 'package:comunic/ui/routes/conversation_route.dart';
|
import 'package:comunic/ui/routes/conversation_route.dart';
|
||||||
import 'package:comunic/ui/routes/main_route/page_info.dart';
|
import 'package:comunic/ui/routes/main_route/page_info.dart';
|
||||||
import 'package:comunic/ui/routes/settings/account_settings_route.dart';
|
import 'package:comunic/ui/routes/settings/account_settings_route.dart';
|
||||||
@ -34,7 +37,7 @@ mixin MainRoute implements StatefulWidget {}
|
|||||||
|
|
||||||
/// Public interface of home controller
|
/// Public interface of home controller
|
||||||
abstract class MainController extends State<MainRoute> {
|
abstract class MainController extends State<MainRoute> {
|
||||||
final _pagesStack = List<PageInfo>();
|
final _pagesStack = <PageInfo>[];
|
||||||
|
|
||||||
/// Default page of the application
|
/// Default page of the application
|
||||||
PageInfo get defaultPage;
|
PageInfo get defaultPage;
|
||||||
@ -151,6 +154,18 @@ abstract class MainController extends State<MainRoute> {
|
|||||||
hideNavBar: true,
|
hideNavBar: true,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
/// Open a conversation message statistics page
|
||||||
|
void openConversationMessageStats(
|
||||||
|
Conversation conv, ConversationMessage message) =>
|
||||||
|
pushPage(PageInfo(
|
||||||
|
child: ConversationMessageStatsRoute(
|
||||||
|
conv: conv,
|
||||||
|
message: message,
|
||||||
|
),
|
||||||
|
hideNavBar: true,
|
||||||
|
canShowAsDialog: true,
|
||||||
|
));
|
||||||
|
|
||||||
/// Start a call for a given conversation
|
/// Start a call for a given conversation
|
||||||
void startCall(int convID) =>
|
void startCall(int convID) =>
|
||||||
pushPage(PageInfo(child: CallScreen(convID: convID), hideNavBar: true));
|
pushPage(PageInfo(child: CallScreen(convID: convID), hideNavBar: true));
|
||||||
|
@ -77,7 +77,7 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(tr("You can choose a new password.")),
|
Text(tr("You can choose a new password.")),
|
||||||
OutlineButton(
|
OutlinedButton(
|
||||||
onPressed: _changePassword,
|
onPressed: _changePassword,
|
||||||
child: Text(tr("Choose a new password")),
|
child: Text(tr("Choose a new password")),
|
||||||
)
|
)
|
||||||
@ -97,7 +97,7 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> {
|
|||||||
tr("Congratulations! Your password has now been successfully changed!"),
|
tr("Congratulations! Your password has now been successfully changed!"),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
OutlineButton(
|
OutlinedButton(
|
||||||
onPressed: _quitScreen,
|
onPressed: _quitScreen,
|
||||||
child: Text(tr("Login")),
|
child: Text(tr("Login")),
|
||||||
)
|
)
|
||||||
|
@ -12,9 +12,13 @@ import 'package:comunic/utils/intl_utils.dart';
|
|||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:identicon/identicon.dart';
|
import 'package:identicon/identicon.dart';
|
||||||
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
import 'package:random_string/random_string.dart';
|
import 'package:random_string/random_string.dart';
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
import 'package:settings_ui/settings_ui.dart';
|
||||||
|
|
||||||
|
import '../../../utils/log_utils.dart';
|
||||||
|
import '../../../utils/ui_utils.dart';
|
||||||
|
|
||||||
/// Account image settings section
|
/// Account image settings section
|
||||||
///
|
///
|
||||||
/// @author Pierre Hubert
|
/// @author Pierre Hubert
|
||||||
@ -156,15 +160,17 @@ class _AccountImageSettingsScreenState
|
|||||||
|
|
||||||
/// Upload a new account image
|
/// Upload a new account image
|
||||||
void _uploadAccountImage() async {
|
void _uploadAccountImage() async {
|
||||||
final image = await pickImage(context);
|
try {
|
||||||
|
final image = await pickImage(context,
|
||||||
|
aspectRatio: CropAspectRatio(ratioX: 5, ratioY: 5));
|
||||||
|
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
if (!await SettingsHelper.uploadAccountImage(image)) {
|
await SettingsHelper.uploadAccountImage(image);
|
||||||
showSimpleSnack(context, tr("Could not upload your account image!"));
|
} catch (e, s) {
|
||||||
return;
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to upload new account image!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
_key.currentState.refresh();
|
_key.currentState.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,9 @@ import 'package:comunic/utils/input_utils.dart';
|
|||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
|
import '../../../models/api_request.dart';
|
||||||
|
import '../../../utils/ui_utils.dart';
|
||||||
|
|
||||||
/// Emojies account settings
|
/// Emojies account settings
|
||||||
///
|
///
|
||||||
@ -136,7 +138,7 @@ class _NewCustomEmojiDialog extends StatefulWidget {
|
|||||||
|
|
||||||
class _NewCustomEmojiDialogState extends State<_NewCustomEmojiDialog> {
|
class _NewCustomEmojiDialogState extends State<_NewCustomEmojiDialog> {
|
||||||
final _controller = TextEditingController();
|
final _controller = TextEditingController();
|
||||||
PickedFile _file;
|
BytesFile _file;
|
||||||
|
|
||||||
bool get _hasImage => _file != null;
|
bool get _hasImage => _file != null;
|
||||||
|
|
||||||
@ -209,6 +211,7 @@ class _NewCustomEmojiDialogState extends State<_NewCustomEmojiDialog> {
|
|||||||
});
|
});
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
print("Could not pick an image! $e\n$stack");
|
print("Could not pick an image! $e\n$stack");
|
||||||
|
snack(context, tr("Failed to pick an image!"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
import 'package:comunic/helpers/conversations_helper.dart';
|
|
||||||
import 'package:comunic/helpers/users_helper.dart';
|
|
||||||
import 'package:comunic/lists/users_list.dart';
|
|
||||||
import 'package:comunic/models/conversation.dart';
|
|
||||||
import 'package:comunic/ui/screens/update_conversation_screen.dart';
|
import 'package:comunic/ui/screens/update_conversation_screen.dart';
|
||||||
import 'package:comunic/ui/widgets/comunic_back_button_widget.dart';
|
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Update a conversation route
|
/// Update a conversation route
|
||||||
@ -24,70 +17,7 @@ class UpdateConversationRoute extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UpdateConversationRoute extends State<UpdateConversationRoute> {
|
class _UpdateConversationRoute extends State<UpdateConversationRoute> {
|
||||||
Conversation _conversation;
|
|
||||||
UsersList _membersInfo;
|
|
||||||
bool _error = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
Widget build(BuildContext context) =>
|
||||||
super.didChangeDependencies();
|
UpdateConversationScreen(convID: widget.conversationID);
|
||||||
_loadConversation();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setError(bool e) => setState(() {
|
|
||||||
_error = e;
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Load information about the being updated conversation
|
|
||||||
Future<void> _loadConversation() async {
|
|
||||||
setError(false);
|
|
||||||
|
|
||||||
final conversation = await ConversationsHelper()
|
|
||||||
.getSingle(widget.conversationID, force: true);
|
|
||||||
|
|
||||||
if (conversation == null) return setError(true);
|
|
||||||
|
|
||||||
//Load information about the members of the conversation
|
|
||||||
_membersInfo = await UsersHelper().getUsersInfo(conversation.members);
|
|
||||||
|
|
||||||
if (_membersInfo == null) return setError(true);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_conversation = conversation;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the body of this widget
|
|
||||||
Widget _buildBody() {
|
|
||||||
if (_error)
|
|
||||||
return buildErrorCard(
|
|
||||||
tr("Could not load information about the conversation"),
|
|
||||||
actions: [
|
|
||||||
FlatButton(
|
|
||||||
onPressed: _loadConversation,
|
|
||||||
child: Text(
|
|
||||||
tr("Retry").toUpperCase(),
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (_conversation == null) return buildLoadingPage();
|
|
||||||
|
|
||||||
return UpdateConversationScreen(
|
|
||||||
initialUsers: _membersInfo,
|
|
||||||
initialSettings: _conversation,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: ComunicBackButton(),
|
|
||||||
title: Text(tr("Update a conversation")),
|
|
||||||
),
|
|
||||||
body: _buildBody(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
68
lib/ui/routes/video_player_route.dart
Normal file
68
lib/ui/routes/video_player_route.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:comunic/ui/widgets/async_screen_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/comunic_back_button_widget.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
/// Video player route
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class VideoPlayerRoute extends StatefulWidget {
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
const VideoPlayerRoute({
|
||||||
|
Key key,
|
||||||
|
@required this.url,
|
||||||
|
}) : assert(url != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_VideoPlayerRouteState createState() => _VideoPlayerRouteState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoPlayerRouteState extends State<VideoPlayerRoute> {
|
||||||
|
VideoPlayerController _videoPlayerController;
|
||||||
|
ChewieController _chewieController;
|
||||||
|
|
||||||
|
Future<void> _initialize() async {
|
||||||
|
_videoPlayerController = VideoPlayerController.network(widget.url);
|
||||||
|
|
||||||
|
await _videoPlayerController.initialize();
|
||||||
|
|
||||||
|
_chewieController = ChewieController(
|
||||||
|
videoPlayerController: _videoPlayerController,
|
||||||
|
looping: false,
|
||||||
|
allowFullScreen: true,
|
||||||
|
allowMuting: true,
|
||||||
|
allowedScreenSleep: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_videoPlayerController != null) _videoPlayerController.dispose();
|
||||||
|
if (_chewieController != null) _chewieController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: ComunicBackButton(),
|
||||||
|
title: Text("Video"),
|
||||||
|
),
|
||||||
|
body: _buildBody(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() => AsyncScreenWidget(
|
||||||
|
onReload: _initialize,
|
||||||
|
onBuild: _showBody,
|
||||||
|
errorMessage: tr("Failed to initialize video!"),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _showBody() => Chewie(controller: _chewieController);
|
||||||
|
}
|
@ -110,7 +110,7 @@ class _CallScreenState extends SafeState<CallScreen> {
|
|||||||
|
|
||||||
// First, load information about the conversation
|
// First, load information about the conversation
|
||||||
_conversation =
|
_conversation =
|
||||||
await ConversationsHelper().getSingleOrThrow(convID, force: true);
|
await ConversationsHelper().getSingle(convID, force: true);
|
||||||
_convName =
|
_convName =
|
||||||
await ConversationsHelper.getConversationNameAsync(_conversation);
|
await ConversationsHelper.getConversationNameAsync(_conversation);
|
||||||
assert(_convName != null);
|
assert(_convName != null);
|
||||||
@ -544,7 +544,7 @@ class _CallScreenState extends SafeState<CallScreen> {
|
|||||||
.where((f) => f.hasVideoStream && _renderers.containsKey(f.userID))
|
.where((f) => f.hasVideoStream && _renderers.containsKey(f.userID))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final rows = List<Row>();
|
final rows = <Row>[];
|
||||||
|
|
||||||
var numberRows = sqrt(availableVideos.length).ceil();
|
var numberRows = sqrt(availableVideos.length).ceil();
|
||||||
var numberCols = numberRows;
|
var numberCols = numberRows;
|
||||||
|
@ -32,8 +32,7 @@ class _ConversationMembersScreenState extends State<ConversationMembersScreen> {
|
|||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
_conversation =
|
_conversation =
|
||||||
await ConversationsHelper().getSingle(widget.convID, force: true);
|
await ConversationsHelper().getSingle(widget.convID, force: true);
|
||||||
_members =
|
_members = await UsersHelper().getListWithThrow(_conversation.membersID);
|
||||||
await UsersHelper().getListWithThrow(_conversation.members.toSet());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -55,12 +54,12 @@ class _ConversationMembersScreenState extends State<ConversationMembersScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildItem(BuildContext context, int index) {
|
Widget _buildItem(BuildContext context, int index) {
|
||||||
final user = _members.getUser(_conversation.members[index]);
|
final member = _conversation.members[index];
|
||||||
|
final user = _members.getUser(member.userID);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: AccountImageWidget(user: user),
|
leading: AccountImageWidget(user: user),
|
||||||
title: Text(user.displayName),
|
title: Text(user.displayName),
|
||||||
subtitle:
|
subtitle: Text(member.isAdmin ? tr("Admin") : tr("Member")),
|
||||||
Text(_conversation.ownerID == user.id ? tr("Owner") : tr("Member")),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,34 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:comunic/helpers/conversations_helper.dart';
|
import 'package:comunic/helpers/conversations_helper.dart';
|
||||||
import 'package:comunic/helpers/events_helper.dart';
|
import 'package:comunic/helpers/events_helper.dart';
|
||||||
|
import 'package:comunic/helpers/server_config_helper.dart';
|
||||||
import 'package:comunic/helpers/users_helper.dart';
|
import 'package:comunic/helpers/users_helper.dart';
|
||||||
import 'package:comunic/lists/conversation_messages_list.dart';
|
import 'package:comunic/lists/conversation_messages_list.dart';
|
||||||
import 'package:comunic/lists/users_list.dart';
|
import 'package:comunic/lists/users_list.dart';
|
||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/conversation.dart';
|
||||||
import 'package:comunic/models/conversation_message.dart';
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
import 'package:comunic/models/new_conversation_message.dart';
|
import 'package:comunic/models/new_conversation_message.dart';
|
||||||
|
import 'package:comunic/ui/dialogs/pick_file_dialog.dart';
|
||||||
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||||
import 'package:comunic/ui/tiles/conversation_message_tile.dart';
|
import 'package:comunic/ui/tiles/conversation_message_tile.dart';
|
||||||
|
import 'package:comunic/ui/tiles/server_conversation_message_tile.dart';
|
||||||
|
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/safe_state.dart';
|
import 'package:comunic/ui/widgets/safe_state.dart';
|
||||||
import 'package:comunic/ui/widgets/scroll_watcher.dart';
|
import 'package:comunic/ui/widgets/scroll_watcher.dart';
|
||||||
|
import 'package:comunic/ui/widgets/user_writing_in_conv_notifier.dart';
|
||||||
|
import 'package:comunic/utils/account_utils.dart';
|
||||||
|
import 'package:comunic/utils/date_utils.dart';
|
||||||
import 'package:comunic/utils/files_utils.dart';
|
import 'package:comunic/utils/files_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:comunic/utils/list_utils.dart';
|
import 'package:comunic/utils/list_utils.dart';
|
||||||
|
import 'package:comunic/utils/log_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
|
import 'package:comunic/utils/video_utils.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:emoji_picker/emoji_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
/// Conversation screen
|
/// Conversation screen
|
||||||
///
|
///
|
||||||
@ -40,15 +55,73 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
final UsersHelper _usersHelper = UsersHelper();
|
final UsersHelper _usersHelper = UsersHelper();
|
||||||
|
|
||||||
// Class members
|
// Class members
|
||||||
|
Conversation _conversation;
|
||||||
ConversationMessagesList _messages;
|
ConversationMessagesList _messages;
|
||||||
UsersList _usersInfo = UsersList();
|
UsersList _usersInfo = UsersList();
|
||||||
ErrorLevel _error = ErrorLevel.NONE;
|
ErrorLevel _error = ErrorLevel.NONE;
|
||||||
bool _isMessageValid = false;
|
final _textFieldFocus = FocusNode();
|
||||||
|
|
||||||
|
bool _showEmojiPicker = false;
|
||||||
|
|
||||||
bool _isSendingMessage = false;
|
bool _isSendingMessage = false;
|
||||||
TextEditingController _textEditingController = TextEditingController();
|
TextEditingController _textController = TextEditingController();
|
||||||
ScrollWatcher _scrollController;
|
ScrollWatcher _scrollController;
|
||||||
_OlderMessagesLevel _loadingOlderMessages = _OlderMessagesLevel.NONE;
|
_OlderMessagesLevel _loadingOlderMessages = _OlderMessagesLevel.NONE;
|
||||||
|
|
||||||
|
int _lastWritingEventSent = 0;
|
||||||
|
|
||||||
|
CancelToken _sendCancel;
|
||||||
|
double _sendProgress;
|
||||||
|
|
||||||
|
String get textMessage => _textController.text;
|
||||||
|
|
||||||
|
bool get _isMessageValid =>
|
||||||
|
textMessage.length >=
|
||||||
|
ServerConfigurationHelper.config.conversationsPolicy.minMessageLen &&
|
||||||
|
textMessage.length <
|
||||||
|
ServerConfigurationHelper.config.conversationsPolicy.maxMessageLen;
|
||||||
|
|
||||||
|
showKeyboard() => _textFieldFocus.requestFocus();
|
||||||
|
|
||||||
|
hideKeyboard() => _textFieldFocus.unfocus();
|
||||||
|
|
||||||
|
hideEmojiContainer() => setState(() => _showEmojiPicker = false);
|
||||||
|
|
||||||
|
showEmojiContainer() => setState(() => _showEmojiPicker = true);
|
||||||
|
|
||||||
|
// Colors definition
|
||||||
|
Color get _senderColor =>
|
||||||
|
_conversation.color ??
|
||||||
|
/*(darkTheme() ? Color(0xff2b343b) :*/ Colors.blue.shade700; //);
|
||||||
|
|
||||||
|
Color get _receiverColor =>
|
||||||
|
darkTheme() ? Color(0xff3a3d40) : Colors.grey.shade600;
|
||||||
|
|
||||||
|
Color get _greyColor => Color(0xff8f8f8f);
|
||||||
|
|
||||||
|
Color get _gradientColorStart =>
|
||||||
|
_conversation.color ??
|
||||||
|
(darkTheme() ? Color(0xff00b6f3) : Colors.blue.shade300);
|
||||||
|
|
||||||
|
Color get _gradientColorEnd =>
|
||||||
|
_conversation.color?.withOpacity(0.7) ??
|
||||||
|
(darkTheme() ? Color(0xff0184dc) : Colors.blueAccent.shade700);
|
||||||
|
|
||||||
|
Color get _separatorColor =>
|
||||||
|
darkTheme() ? Color(0xff272c35) : Color(0xffBEBEBE);
|
||||||
|
|
||||||
|
LinearGradient get _fabGradient => LinearGradient(
|
||||||
|
colors: [_gradientColorStart, _gradientColorEnd],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
);
|
||||||
|
|
||||||
|
LinearGradient get _disabledGradient => LinearGradient(
|
||||||
|
colors: [_greyColor, _receiverColor],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -70,14 +143,16 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/// Method called when an error occurred while loading messages
|
/// Method called when an error occurred while loading messages
|
||||||
void _errorLoading() {
|
void _errorLoading() =>
|
||||||
_setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR);
|
_setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR);
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the first conversations
|
/// Load the first conversations
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
_scrollController = ScrollWatcher(onReachBottom: _loadOlderMessages);
|
_scrollController = ScrollWatcher(onReachBottom: _loadOlderMessages);
|
||||||
|
|
||||||
|
_conversation =
|
||||||
|
await ConversationsHelper().getSingle(widget.conversationID);
|
||||||
|
|
||||||
// Fetch latest messages
|
// Fetch latest messages
|
||||||
await _loadMessages(false);
|
await _loadMessages(false);
|
||||||
await _loadMessages(true);
|
await _loadMessages(true);
|
||||||
@ -86,25 +161,42 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
.registerConversationEvents(widget.conversationID);
|
.registerConversationEvents(widget.conversationID);
|
||||||
|
|
||||||
this.listen<NewConversationMessageEvent>((ev) async {
|
this.listen<NewConversationMessageEvent>((ev) async {
|
||||||
if (ev.msg.conversationID == widget.conversationID) {
|
if (ev.msg.convID == widget.conversationID) {
|
||||||
|
try {
|
||||||
await _conversationsHelper.saveMessage(ev.msg);
|
await _conversationsHelper.saveMessage(ev.msg);
|
||||||
await _applyNewMessages(ConversationMessagesList()..add(ev.msg));
|
await _applyNewMessages(ConversationMessagesList()..add(ev.msg));
|
||||||
|
} catch (e, s) {
|
||||||
|
print("Failed to show new message! $e => $s");
|
||||||
|
_errorLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.listen<UpdatedConversationMessageEvent>((ev) async {
|
this.listen<UpdatedConversationMessageEvent>((ev) async {
|
||||||
if (ev.msg.conversationID == widget.conversationID) {
|
if (ev.msg.convID == widget.conversationID) {
|
||||||
await _conversationsHelper.saveMessage(ev.msg);
|
await _conversationsHelper.saveMessage(ev.msg);
|
||||||
setState(() => _messages.replace(ev.msg));
|
setState(() => _messages.replace(ev.msg));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.listen<DeletedConversationMessageEvent>((ev) async {
|
this.listen<DeletedConversationMessageEvent>((ev) async {
|
||||||
if (ev.msg.conversationID == widget.conversationID) {
|
if (ev.msg.convID == widget.conversationID) {
|
||||||
await _conversationsHelper.removeMessage(ev.msg.id);
|
await _conversationsHelper.removeMessage(ev.msg);
|
||||||
setState(() => _messages.removeMsg(ev.msg.id));
|
setState(() => _messages.removeMsg(ev.msg.id));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.listen<RemovedUserFromConversationEvent>((ev) {
|
||||||
|
if (ev.userID == userID() && ev.convID == widget.conversationID) {
|
||||||
|
setState(() => _error = ErrorLevel.MAJOR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.listen<DeletedConversationEvent>((ev) {
|
||||||
|
if (ev.convID == widget.conversationID) {
|
||||||
|
setState(() => _error = ErrorLevel.MAJOR);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Free resources when this conversation widget is no longer required
|
/// Free resources when this conversation widget is no longer required
|
||||||
@ -116,19 +208,23 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
Future<void> _loadMessages(bool online) async {
|
Future<void> _loadMessages(bool online) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
//First, get the messages
|
//First, get the messages
|
||||||
final messages = await _conversationsHelper.getNewMessages(
|
final messages = await _conversationsHelper.getNewMessages(
|
||||||
conversationID: widget.conversationID,
|
conversationID: widget.conversationID,
|
||||||
lastMessageID: _messages == null ? 0 : _messages.lastMessageID,
|
lastMessageID: _messages == null ? 0 : _messages.lastMessageID,
|
||||||
online: online);
|
online: online,
|
||||||
|
);
|
||||||
if (messages == null) return _errorLoading();
|
|
||||||
|
|
||||||
// In case we are offline and we did not get any message we do not do
|
// In case we are offline and we did not get any message we do not do
|
||||||
// anything (we wait for the online request)
|
// anything (we wait for the online request)
|
||||||
if (messages.length == 0 && !online) return;
|
if (messages.length == 0 && !online) return;
|
||||||
|
|
||||||
await _applyNewMessages(messages);
|
await _applyNewMessages(messages);
|
||||||
|
} catch (e, s) {
|
||||||
|
debugPrint("Failed to load messages! $e => $s", wrapWidth: 4096);
|
||||||
|
_errorLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get older messages
|
/// Get older messages
|
||||||
@ -136,7 +232,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
if (_loadingOlderMessages != _OlderMessagesLevel.NONE ||
|
if (_loadingOlderMessages != _OlderMessagesLevel.NONE ||
|
||||||
_messages == null ||
|
_messages == null ||
|
||||||
_messages.length == 0) return;
|
_messages.length == 0) return;
|
||||||
|
try {
|
||||||
// Let's start to load older messages
|
// Let's start to load older messages
|
||||||
_setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING);
|
_setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING);
|
||||||
|
|
||||||
@ -147,12 +243,6 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
// Mark as not loading anymore
|
// Mark as not loading anymore
|
||||||
_setLoadingOlderMessagesState(_OlderMessagesLevel.NONE);
|
_setLoadingOlderMessagesState(_OlderMessagesLevel.NONE);
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
if (messages == null) {
|
|
||||||
_errorLoading();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there is no more unread messages
|
// Check if there is no more unread messages
|
||||||
if (messages.length == 0) {
|
if (messages.length == 0) {
|
||||||
_setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE);
|
_setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE);
|
||||||
@ -161,20 +251,24 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
|
|
||||||
// Apply the messages
|
// Apply the messages
|
||||||
_applyNewMessages(messages);
|
_applyNewMessages(messages);
|
||||||
|
} catch (e, s) {
|
||||||
|
print("Failed to load older messages! $e => $s");
|
||||||
|
_errorLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply new messages [messages] must not be null
|
/// Apply new messages [messages] must not be null
|
||||||
|
///
|
||||||
|
/// Throws in case of failure
|
||||||
Future<void> _applyNewMessages(ConversationMessagesList messages) async {
|
Future<void> _applyNewMessages(ConversationMessagesList messages) async {
|
||||||
// We ignore new messages once the area is no longer visible
|
// We ignore new messages once the area is no longer visible
|
||||||
if (!this.mounted) return;
|
if (!this.mounted) return;
|
||||||
|
|
||||||
//Then get information about users
|
//Then get information about users
|
||||||
final usersToGet =
|
final usersToGet =
|
||||||
findMissingFromList(_usersInfo.usersID, messages.getUsersID());
|
findMissingFromSet(_usersInfo.usersID.toSet(), messages.getUsersID());
|
||||||
|
|
||||||
final users = await _usersHelper.getUsersInfo(usersToGet);
|
final users = await _usersHelper.getList(usersToGet);
|
||||||
|
|
||||||
if (users == null) _errorLoading();
|
|
||||||
|
|
||||||
// Save the new list of messages
|
// Save the new list of messages
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -197,39 +291,63 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
_setError(ErrorLevel.NONE);
|
_setError(ErrorLevel.NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pick and send an image
|
/// Send a file message
|
||||||
Future<void> _sendImage(BuildContext context) async {
|
Future<void> _sendFileMessage() async {
|
||||||
final image = await pickImage(context);
|
try {
|
||||||
|
final file = await showPickFileDialog(
|
||||||
|
context: context,
|
||||||
|
maxFileSize: srvConfig.conversationsPolicy.filesMaxSize,
|
||||||
|
allowedMimeTypes: srvConfig.conversationsPolicy.allowedFilesType,
|
||||||
|
imageMaxWidth: srvConfig.conversationsPolicy.maxMessageImageWidth,
|
||||||
|
imageMaxHeight: srvConfig.conversationsPolicy.maxMessageImageHeight,
|
||||||
|
);
|
||||||
|
|
||||||
if (image == null) return null;
|
if (file == null) return;
|
||||||
|
|
||||||
_submitMessage(
|
BytesFile thumbnail;
|
||||||
context,
|
|
||||||
|
if (isVideo(lookupMimeType(file.filename)))
|
||||||
|
thumbnail = await generateVideoThumbnail(
|
||||||
|
videoFile: file,
|
||||||
|
maxWidth: srvConfig.conversationsPolicy.maxThumbnailWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
_sendCancel = CancelToken();
|
||||||
|
final progressCb =
|
||||||
|
(count, total) => setState(() => _sendProgress = count / total);
|
||||||
|
final res = await ConversationsHelper().sendMessage(
|
||||||
NewConversationMessage(
|
NewConversationMessage(
|
||||||
conversationID: widget.conversationID,
|
conversationID: widget.conversationID,
|
||||||
message: null,
|
message: null,
|
||||||
image: image,
|
file: file,
|
||||||
),
|
thumbnail: thumbnail),
|
||||||
|
sendProgress: progressCb,
|
||||||
|
cancelToken: _sendCancel,
|
||||||
);
|
);
|
||||||
|
assert(res == SendMessageResult.SUCCESS);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
showSimpleSnack(context, tr("Failed to send a file!"));
|
||||||
|
}
|
||||||
|
|
||||||
// In case a message was already written in the input
|
setState(() {
|
||||||
_updatedText(_textEditingController.text);
|
_sendCancel = null;
|
||||||
|
_sendProgress = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a new text message
|
/// Send a new text message
|
||||||
Future<void> _submitTextMessage(BuildContext context, String content) async {
|
Future<void> _submitTextMessage() async {
|
||||||
if (await _submitMessage(
|
if (await _submitMessage(NewConversationMessage(
|
||||||
context,
|
|
||||||
NewConversationMessage(
|
|
||||||
conversationID: widget.conversationID,
|
conversationID: widget.conversationID,
|
||||||
message: content,
|
message: textMessage,
|
||||||
)) ==
|
)) ==
|
||||||
SendMessageResult.SUCCESS) _clearSendMessageForm();
|
SendMessageResult.SUCCESS) _clearSendMessageForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Submit a new message
|
/// Submit a new message
|
||||||
Future<SendMessageResult> _submitMessage(
|
Future<SendMessageResult> _submitMessage(
|
||||||
BuildContext context, NewConversationMessage message) async {
|
NewConversationMessage message) async {
|
||||||
//Send the message
|
//Send the message
|
||||||
_setSending(true);
|
_setSending(true);
|
||||||
final result = await _conversationsHelper.sendMessage(message);
|
final result = await _conversationsHelper.sendMessage(message);
|
||||||
@ -237,7 +355,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
|
|
||||||
//Check the result of the operation
|
//Check the result of the operation
|
||||||
if (result != SendMessageResult.SUCCESS)
|
if (result != SendMessageResult.SUCCESS)
|
||||||
Scaffold.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
result == SendMessageResult.MESSAGE_REJECTED
|
result == SendMessageResult.MESSAGE_REJECTED
|
||||||
@ -251,31 +369,9 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updatedText(String text) {
|
|
||||||
setState(() {
|
|
||||||
_isMessageValid = text.length > 2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear send message form
|
/// Clear send message form
|
||||||
void _clearSendMessageForm() {
|
void _clearSendMessageForm() {
|
||||||
setState(() {
|
setState(() => _textController = TextEditingController());
|
||||||
_textEditingController = TextEditingController();
|
|
||||||
_isMessageValid = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a message is the last message of a user or not
|
|
||||||
bool _isLastMessage(int index) {
|
|
||||||
return index == 0 ||
|
|
||||||
(index > 0 && _messages[index - 1].userID != _messages[index].userID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a message is the first message of a user or not
|
|
||||||
bool _isFirstMessage(int index) {
|
|
||||||
return index == _messages.length - 1 ||
|
|
||||||
(index < _messages.length - 1 &&
|
|
||||||
_messages[index + 1].userID != _messages[index].userID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error handling
|
/// Error handling
|
||||||
@ -295,7 +391,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
Widget _buildNoMessagesNotice() {
|
Widget _buildNoMessagesNotice() {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(tr("There is no message yet in this converation.")),
|
child: Text(tr("There is no message yet in this conversation.")),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -307,82 +403,248 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
itemCount: _messages.length,
|
itemCount: _messages.length,
|
||||||
itemBuilder: (c, i) {
|
itemBuilder: (c, i) => _buildMessageItem(i),
|
||||||
return ConversationMessageTile(
|
));
|
||||||
message: _messages.elementAt(i),
|
}
|
||||||
userInfo: _usersInfo.getUser(_messages[i].userID),
|
|
||||||
isLastMessage: _isLastMessage(i),
|
Widget _buildMessageItem(int msgIndex) {
|
||||||
isFirstMessage: _isFirstMessage(i),
|
final msg = _messages[msgIndex];
|
||||||
onRequestMessageUpdate: _updateMessage,
|
final nextMessage =
|
||||||
onRequestMessageDelete: _deleteMessage,
|
msgIndex + 1 < _messages.length ? _messages[msgIndex + 1] : null;
|
||||||
);
|
|
||||||
}),
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
child: !isSameDate(msg.date, nextMessage?.date)
|
||||||
|
? _buildDateWidget(msg.date)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
msg.isServerMessage
|
||||||
|
? Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: ServerConversationMessageTile(
|
||||||
|
message: msg.serverMessage, users: _usersInfo),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
margin: EdgeInsets.symmetric(vertical: 5),
|
||||||
|
alignment:
|
||||||
|
msg.isOwner ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
|
child: msg.isOwner
|
||||||
|
? _buildSenderLayout(msg, nextMessage)
|
||||||
|
: _buildReceiverLayout(msg, nextMessage),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send message form
|
Widget _buildSenderLayout(
|
||||||
Widget _buildSendMessageForm() {
|
ConversationMessage message, ConversationMessage previousMessage) {
|
||||||
return new Container(
|
final messageRadius = Radius.circular(10);
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: new Row(
|
return Container(
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: previousMessage?.isOwner == true ? 0 : 12, right: 5),
|
||||||
|
constraints:
|
||||||
|
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.65),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _senderColor,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: messageRadius,
|
||||||
|
topRight: messageRadius,
|
||||||
|
bottomLeft: messageRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: _buildMessage(message),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReceiverLayout(
|
||||||
|
ConversationMessage message, ConversationMessage previousMessage) {
|
||||||
|
final messageRadius = Radius.circular(10);
|
||||||
|
|
||||||
|
return Row(children: [
|
||||||
|
SizedBox(width: 5),
|
||||||
|
AccountImageWidget(
|
||||||
|
user: _usersInfo.getUser(message.userID),
|
||||||
|
),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: previousMessage == null ||
|
||||||
|
message.userID != previousMessage.userID
|
||||||
|
? 12
|
||||||
|
: 0),
|
||||||
|
constraints:
|
||||||
|
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.65),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _receiverColor,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomRight: messageRadius,
|
||||||
|
topRight: messageRadius,
|
||||||
|
bottomLeft: messageRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: _buildMessage(message),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMessage(ConversationMessage msg) => ConversationMessageTile(
|
||||||
|
message: msg,
|
||||||
|
user: _usersInfo.getUser(msg.userID),
|
||||||
|
onRequestMessageStats: _requestMessageStats,
|
||||||
|
onRequestMessageUpdate: _updateMessage,
|
||||||
|
onRequestMessageDelete: _deleteMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildDateWidget(DateTime dt) => Center(
|
||||||
|
child: Container(
|
||||||
|
child: Text(
|
||||||
|
formatDisplayDate(dt, time: false),
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.only(top: 50, bottom: 5),
|
||||||
|
));
|
||||||
|
|
||||||
|
/// Send new message form
|
||||||
|
Widget _buildSendMessageForm() => Container(
|
||||||
|
padding: EdgeInsets.fromLTRB(10, 5, 10, 5),
|
||||||
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Image area
|
GestureDetector(
|
||||||
new Container(
|
onTap: !_isSendingMessage ? _sendFileMessage : null,
|
||||||
margin: new EdgeInsets.symmetric(horizontal: 4.0),
|
child: Container(
|
||||||
child: new IconButton(
|
padding: EdgeInsets.all(6),
|
||||||
icon: new Icon(
|
decoration: BoxDecoration(
|
||||||
Icons.photo_camera,
|
gradient:
|
||||||
color: _isSendingMessage
|
_isSendingMessage ? _disabledGradient : _fabGradient,
|
||||||
? Theme.of(context).disabledColor
|
shape: BoxShape.circle,
|
||||||
: Theme.of(context).accentColor,
|
|
||||||
),
|
),
|
||||||
onPressed: () => _sendImage(context),
|
child: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// Message area
|
SizedBox(width: 5),
|
||||||
new Flexible(
|
Expanded(
|
||||||
child: new TextField(
|
child: Stack(
|
||||||
keyboardType: TextInputType.text,
|
alignment: Alignment.centerRight,
|
||||||
maxLines: null,
|
children: [
|
||||||
maxLength: 200,
|
TextField(
|
||||||
maxLengthEnforced: true,
|
|
||||||
|
|
||||||
// Show max length only when there is some text already typed
|
|
||||||
buildCounter: smartInputCounterWidgetBuilder,
|
|
||||||
|
|
||||||
enabled: !_isSendingMessage,
|
enabled: !_isSendingMessage,
|
||||||
controller: _textEditingController,
|
maxLines: 10,
|
||||||
onChanged: _updatedText,
|
minLines: 1,
|
||||||
onSubmitted: _isMessageValid
|
controller: _textController,
|
||||||
? (s) => _submitTextMessage(context, s)
|
focusNode: _textFieldFocus,
|
||||||
: null,
|
onTap: () => hideEmojiContainer(),
|
||||||
decoration: new InputDecoration.collapsed(
|
textInputAction: TextInputAction.send,
|
||||||
hintText: tr("Send a message"),
|
onSubmitted: (s) => _submitTextMessage(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: darkTheme() ? Colors.white : Colors.black,
|
||||||
|
),
|
||||||
|
onChanged: (s) {
|
||||||
|
_sendWritingEvent();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: tr("New message..."),
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: _greyColor,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
const Radius.circular(50.0),
|
||||||
|
),
|
||||||
|
borderSide: BorderSide.none),
|
||||||
|
contentPadding: EdgeInsets.fromLTRB(20, 8, 32, 8),
|
||||||
|
filled: true,
|
||||||
|
fillColor: _separatorColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
IconButton(
|
||||||
|
splashColor: Colors.transparent,
|
||||||
// Send button
|
highlightColor: Colors.transparent,
|
||||||
new Container(
|
onPressed: () {
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
if (!_showEmojiPicker) {
|
||||||
child: new IconButton(
|
// keyboard is visible
|
||||||
icon: new Icon(
|
hideKeyboard();
|
||||||
Icons.send,
|
Future.delayed(Duration(milliseconds: 100),
|
||||||
color: !_isSendingMessage && _isMessageValid
|
() => showEmojiContainer());
|
||||||
? Theme.of(context).accentColor
|
} else {
|
||||||
: Theme.of(context).disabledColor,
|
//keyboard is hidden
|
||||||
),
|
showKeyboard();
|
||||||
onPressed: !_isSendingMessage && _isMessageValid
|
hideEmojiContainer();
|
||||||
? () =>
|
}
|
||||||
_submitTextMessage(context, _textEditingController.text)
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.face,
|
||||||
|
color: _showEmojiPicker
|
||||||
|
? (_conversation.color ?? Colors.blue)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _isMessageValid ? _submitTextMessage : null,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: !_isMessageValid ? _disabledGradient : _fabGradient,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.send,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildEmojiContainer() => EmojiPicker(
|
||||||
|
bgColor: _conversation.color ?? Colors.blue.shade900,
|
||||||
|
indicatorColor: _conversation.color ?? Colors.blue.shade900,
|
||||||
|
rows: 3,
|
||||||
|
columns: 7,
|
||||||
|
onEmojiSelected: (emoji, category) {
|
||||||
|
_textController.text = _textController.text + emoji.emoji;
|
||||||
|
},
|
||||||
|
recommendKeywords: ["face", "happy", "party", "sad"],
|
||||||
|
numRecommended: 50,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSendingWidget() => Container(
|
||||||
|
height: 68,
|
||||||
|
color: _senderColor,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Spacer(flex: 1),
|
||||||
|
Flexible(
|
||||||
|
child: LinearProgressIndicator(value: _sendProgress),
|
||||||
|
flex: 5,
|
||||||
|
),
|
||||||
|
Spacer(flex: 1),
|
||||||
|
Text("${(_sendProgress * 100).toInt()}%"),
|
||||||
|
Spacer(flex: 1),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => _sendCancel.cancel(),
|
||||||
|
child: Text(tr("Cancel").toUpperCase()),
|
||||||
|
),
|
||||||
|
Spacer(flex: 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -401,12 +663,34 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
_messages.length == 0 ? _buildNoMessagesNotice() : _buildMessagesList(),
|
_messages.length == 0 ? _buildNoMessagesNotice() : _buildMessagesList(),
|
||||||
Divider(),
|
UserWritingInConvNotifier(convID: _conversation.id),
|
||||||
_buildSendMessageForm()
|
_sendCancel != null ? _buildSendingWidget() : _buildSendMessageForm(),
|
||||||
|
_showEmojiPicker ? _buildEmojiContainer() : Container(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _sendWritingEvent() async {
|
||||||
|
try {
|
||||||
|
if (textMessage.isEmpty) return;
|
||||||
|
final t = time();
|
||||||
|
|
||||||
|
if (t - _lastWritingEventSent <
|
||||||
|
srvConfig.conversationsPolicy.writingEventInterval) return;
|
||||||
|
|
||||||
|
_lastWritingEventSent = t;
|
||||||
|
await ConversationsHelper.sendWritingEvent(_conversation.id);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request message statistics
|
||||||
|
void _requestMessageStats(ConversationMessage message) async {
|
||||||
|
MainController.of(context)
|
||||||
|
.openConversationMessageStats(_conversation, message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Request message content update
|
/// Request message content update
|
||||||
Future<void> _updateMessage(ConversationMessage message) async {
|
Future<void> _updateMessage(ConversationMessage message) async {
|
||||||
final newContent = await askUserString(
|
final newContent = await askUserString(
|
||||||
@ -414,7 +698,12 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
title: tr("Update message"),
|
title: tr("Update message"),
|
||||||
message: tr("Please enter new message content:"),
|
message: tr("Please enter new message content:"),
|
||||||
defaultValue: message.message.content,
|
defaultValue: message.message.content,
|
||||||
hint: tr("New message"));
|
hint: tr("New message"),
|
||||||
|
minLength:
|
||||||
|
ServerConfigurationHelper.config.conversationsPolicy.minMessageLen,
|
||||||
|
maxLength:
|
||||||
|
ServerConfigurationHelper.config.conversationsPolicy.maxMessageLen,
|
||||||
|
);
|
||||||
|
|
||||||
if (newContent == null) return;
|
if (newContent == null) return;
|
||||||
|
|
||||||
@ -435,13 +724,13 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
|||||||
textAlign: TextAlign.justify,
|
textAlign: TextAlign.justify,
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Cancel").toUpperCase(),
|
tr("Cancel").toUpperCase(),
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.pop(c, false),
|
onPressed: () => Navigator.pop(c, false),
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Confirm").toUpperCase(),
|
tr("Confirm").toUpperCase(),
|
||||||
style: TextStyle(color: Colors.red),
|
style: TextStyle(color: Colors.red),
|
||||||
|
@ -62,32 +62,24 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
|
|||||||
await _loadConversationsList(false);
|
await _loadConversationsList(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _gotLoadingError() {
|
|
||||||
setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the list of conversations
|
/// Load the list of conversations
|
||||||
Future<void> _loadConversationsList(bool cached) async {
|
Future<void> _loadConversationsList(bool cached) async {
|
||||||
setError(LoadErrorLevel.NONE);
|
setError(LoadErrorLevel.NONE);
|
||||||
|
|
||||||
//Get the list of conversations
|
try {
|
||||||
var list;
|
ConversationsList list = cached
|
||||||
if (cached)
|
? await _conversationsHelper.getCachedList()
|
||||||
list = await _conversationsHelper.getCachedList();
|
: await _conversationsHelper.downloadList();
|
||||||
else
|
assert(list != null);
|
||||||
list = await _conversationsHelper.downloadList();
|
|
||||||
|
|
||||||
if (list == null) return _gotLoadingError();
|
|
||||||
|
|
||||||
//Get information about the members of the conversations
|
//Get information about the members of the conversations
|
||||||
list.users = await _usersHelper.getUsersInfo(list.allUsersID);
|
list.users = await _usersHelper.getList(list.allUsersID);
|
||||||
|
|
||||||
if (list.users == null) return _gotLoadingError();
|
setState(() => _list = list);
|
||||||
|
} catch (e, s) {
|
||||||
//Save list
|
debugPrint("Failed to get conversations list! $e => $s", wrapWidth: 1024);
|
||||||
setState(() {
|
setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR);
|
||||||
_list = list;
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an error card
|
/// Build an error card
|
||||||
@ -95,7 +87,7 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
|
|||||||
return buildErrorCard(
|
return buildErrorCard(
|
||||||
tr("Could not retrieve the list of conversations!"),
|
tr("Could not retrieve the list of conversations!"),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
onPressed: () => _refreshIndicatorKey.currentState.show(),
|
onPressed: () => _refreshIndicatorKey.currentState.show(),
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Retry").toUpperCase(),
|
tr("Retry").toUpperCase(),
|
||||||
@ -131,37 +123,24 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle conversation deletion request
|
/// Handle conversation deletion request
|
||||||
Future<void> _requestDeleteConversation(Conversation conversation) async {
|
Future<void> _requestLeaveConversation(Conversation conversation) async {
|
||||||
final result = await showDialog<bool>(
|
final result = await showConfirmDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (c) {
|
message: conversation.isLastAdmin
|
||||||
return AlertDialog(
|
? tr(
|
||||||
title: Text(tr("Delete conversation")),
|
"Do you really want to leave this conversation ? As you are its last admin, it will be completely deleted!")
|
||||||
content: Text(tr(
|
: tr("Do you really want to leave this conversation ?"));
|
||||||
"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!")),
|
|
||||||
actions: <Widget>[
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
child: Text(tr("cancel").toUpperCase()),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
child: Text(
|
|
||||||
tr("delete").toUpperCase(),
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == null || !result) return;
|
if (result == null || !result) return;
|
||||||
|
|
||||||
// Request the conversation to be deleted now
|
// Request the conversation to be deleted now
|
||||||
if (!await _conversationsHelper.deleteConversation(conversation.id))
|
try {
|
||||||
Scaffold.of(context).showSnackBar(
|
await _conversationsHelper.deleteConversation(conversation.id);
|
||||||
SnackBar(content: Text(tr("Could not delete the conversation!"))));
|
} catch (e, s) {
|
||||||
|
print("Failed to leave conversation! $e => $s");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(tr("Could not leave the conversation!"))));
|
||||||
|
}
|
||||||
|
|
||||||
// Reload the list of conversations
|
// Reload the list of conversations
|
||||||
_loadConversationsList(false);
|
_loadConversationsList(false);
|
||||||
@ -195,7 +174,7 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
|
|||||||
_openConversation(c.id);
|
_openConversation(c.id);
|
||||||
},
|
},
|
||||||
onRequestUpdate: _updateConversation,
|
onRequestUpdate: _updateConversation,
|
||||||
onRequestDelete: _requestDeleteConversation,
|
onRequestLeave: _requestLeaveConversation,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: _list.length,
|
itemCount: _list.length,
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import 'package:comunic/ui/screens/update_conversation_screen.dart';
|
import 'package:comunic/ui/screens/update_conversation_screen.dart';
|
||||||
import 'package:comunic/ui/widgets/comunic_back_button_widget.dart';
|
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Create a new conversation route
|
/// Create a new conversation route
|
||||||
@ -9,13 +7,5 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class CreateConversationScreen extends StatelessWidget {
|
class CreateConversationScreen extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => UpdateConversationScreen();
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: ComunicBackButton(),
|
|
||||||
title: Text(tr("Create a conversation")),
|
|
||||||
),
|
|
||||||
body: UpdateConversationScreen(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ class _FriendsListScreenState extends SafeState<FriendsListScreen> {
|
|||||||
Widget _buildError() => buildErrorCard(
|
Widget _buildError() => buildErrorCard(
|
||||||
tr("Could not load your list of friends!"),
|
tr("Could not load your list of friends!"),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
onPressed: _refreshList,
|
onPressed: _refreshList,
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Retry").toUpperCase(),
|
tr("Retry").toUpperCase(),
|
||||||
@ -171,11 +171,11 @@ class _FriendsListScreenState extends SafeState<FriendsListScreen> {
|
|||||||
content: Text(tr(
|
content: Text(tr(
|
||||||
"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!")),
|
"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!")),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, false),
|
onPressed: () => Navigator.pop(context, false),
|
||||||
child: Text(tr("Cancel").toUpperCase()),
|
child: Text(tr("Cancel").toUpperCase()),
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, true),
|
onPressed: () => Navigator.pop(context, true),
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Confirm").toUpperCase(),
|
tr("Confirm").toUpperCase(),
|
||||||
|
@ -259,19 +259,19 @@ class _GroupSettingsScreenState extends SafeState<GroupSettingsScreen> {
|
|||||||
// Upload a new logo
|
// Upload a new logo
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: tr("Upload a new logo"),
|
title: tr("Upload a new logo"),
|
||||||
onPressed: (_) => _uploadNewLogo,
|
onPressed: (_) => _uploadNewLogo(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Generate a new random logo
|
// Generate a new random logo
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: tr("Generate a new random logo"),
|
title: tr("Generate a new random logo"),
|
||||||
onPressed: (_) => _generateRandomLogo,
|
onPressed: (_) => _generateRandomLogo(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Delete current logo
|
// Delete current logo
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: tr("Delete logo"),
|
title: tr("Delete logo"),
|
||||||
onPressed: (_) => _deleteLogo,
|
onPressed: (_) => _deleteLogo(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -281,8 +281,8 @@ class _GroupSettingsScreenState extends SafeState<GroupSettingsScreen> {
|
|||||||
void _uploadNewLogo() async {
|
void _uploadNewLogo() async {
|
||||||
try {
|
try {
|
||||||
final logo = await pickImage(context);
|
final logo = await pickImage(context);
|
||||||
final bytes = await logo.readAsBytes();
|
if (logo == null) return;
|
||||||
await _doUploadLogo(bytes);
|
await _doUploadLogo(logo.bytes);
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
print("Could not upload new logo! $e\n$stack");
|
print("Could not upload new logo! $e\n$stack");
|
||||||
showSimpleSnack(context, tr("Could not upload new logo!"));
|
showSimpleSnack(context, tr("Could not upload new logo!"));
|
||||||
@ -328,7 +328,7 @@ class _GroupSettingsScreenState extends SafeState<GroupSettingsScreen> {
|
|||||||
tiles: [
|
tiles: [
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: tr("Delete group"),
|
title: tr("Delete group"),
|
||||||
onPressed: (_) => _deleteGroup,
|
onPressed: (_) => _deleteGroup(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -97,7 +97,7 @@ class _OtherUserFriendsListScreenState
|
|||||||
"Could not get the list of friends of this user !",
|
"Could not get the list of friends of this user !",
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Try again").toUpperCase(),
|
tr("Try again").toUpperCase(),
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white),
|
||||||
|
@ -4,8 +4,8 @@ import 'package:comunic/helpers/users_helper.dart';
|
|||||||
import 'package:comunic/lists/unread_conversations_list.dart';
|
import 'package:comunic/lists/unread_conversations_list.dart';
|
||||||
import 'package:comunic/lists/users_list.dart';
|
import 'package:comunic/lists/users_list.dart';
|
||||||
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||||
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
|
||||||
import 'package:comunic/ui/widgets/async_screen_widget.dart';
|
import 'package:comunic/ui/widgets/async_screen_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/conversation_image_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/safe_state.dart';
|
import 'package:comunic/ui/widgets/safe_state.dart';
|
||||||
import 'package:comunic/utils/date_utils.dart';
|
import 'package:comunic/utils/date_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
@ -71,21 +71,40 @@ class _UnreadConversationsScreenState
|
|||||||
|
|
||||||
Widget _tileBuilder(BuildContext context, int index) {
|
Widget _tileBuilder(BuildContext context, int index) {
|
||||||
final conv = _list[index];
|
final conv = _list[index];
|
||||||
final user = _users.getUser(conv.userID);
|
|
||||||
|
final message = _list[index].message;
|
||||||
|
|
||||||
|
final singleUserConv = conv.conv.members.length < 3;
|
||||||
|
|
||||||
|
String messageStr;
|
||||||
|
if (message.hasFile)
|
||||||
|
messageStr = tr("New file");
|
||||||
|
else if (message.hasMessage)
|
||||||
|
messageStr = singleUserConv
|
||||||
|
? message.message.content
|
||||||
|
: tr("%1% : %2%", args: {
|
||||||
|
"1": _users.getUser(message.userID).fullName,
|
||||||
|
"2": message.message.content,
|
||||||
|
});
|
||||||
|
else
|
||||||
|
message.serverMessage.getText(_users);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: AccountImageWidget(user: user),
|
leading: ConversationImageWidget(
|
||||||
title: Text(user.displayName),
|
conversation: conv.conv,
|
||||||
|
users: _users,
|
||||||
|
),
|
||||||
|
title: Text(ConversationsHelper.getConversationName(conv.conv, _users)),
|
||||||
subtitle: RichText(
|
subtitle: RichText(
|
||||||
text: TextSpan(style: Theme.of(context).textTheme.bodyText2, children: [
|
text: TextSpan(style: Theme.of(context).textTheme.bodyText2, children: [
|
||||||
TextSpan(text: conv.convName.isNotEmpty ? conv.convName + "\n" : ""),
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: conv.message,
|
text: messageStr,
|
||||||
style: TextStyle(fontStyle: FontStyle.italic),
|
style: TextStyle(fontStyle: FontStyle.italic),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
trailing: Text(diffTimeFromNowToStr(conv.lastActive)),
|
trailing: Text(diffTimeFromNowToStr(conv.message.timeSent)),
|
||||||
onTap: () => MainController.of(context).openConversation(conv.id),
|
onTap: () => MainController.of(context).openConversation(conv.conv.id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,39 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:comunic/helpers/conversations_helper.dart';
|
import 'package:comunic/helpers/conversations_helper.dart';
|
||||||
|
import 'package:comunic/helpers/server_config_helper.dart';
|
||||||
|
import 'package:comunic/helpers/users_helper.dart';
|
||||||
import 'package:comunic/lists/users_list.dart';
|
import 'package:comunic/lists/users_list.dart';
|
||||||
import 'package:comunic/models/conversation.dart';
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
import 'package:comunic/models/new_conversation.dart';
|
||||||
|
import 'package:comunic/models/new_conversation_settings.dart';
|
||||||
import 'package:comunic/models/user.dart';
|
import 'package:comunic/models/user.dart';
|
||||||
|
import 'package:comunic/ui/dialogs/color_picker_dialog.dart';
|
||||||
|
import 'package:comunic/ui/dialogs/pick_file_dialog.dart';
|
||||||
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||||
import 'package:comunic/ui/tiles/simple_user_tile.dart';
|
import 'package:comunic/ui/tiles/simple_user_tile.dart';
|
||||||
|
import 'package:comunic/ui/widgets/async_screen_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/comunic_back_button_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/pick_user_widget.dart';
|
import 'package:comunic/ui/widgets/pick_user_widget.dart';
|
||||||
|
import 'package:comunic/utils/account_utils.dart';
|
||||||
|
import 'package:comunic/utils/color_utils.dart';
|
||||||
|
import 'package:comunic/utils/dart_color.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:comunic/utils/log_utils.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Create / Update conversation screen
|
/// Create / Update conversation screen
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
enum _MembersMenuChoices { REMOVE }
|
enum _MembersMenuChoices { TOGGLE_ADMIN_STATUS, REMOVE }
|
||||||
|
|
||||||
class UpdateConversationScreen extends StatefulWidget {
|
class UpdateConversationScreen extends StatefulWidget {
|
||||||
final Conversation initialSettings;
|
final convID;
|
||||||
final UsersList initialUsers;
|
|
||||||
|
|
||||||
const UpdateConversationScreen({
|
const UpdateConversationScreen({
|
||||||
Key key,
|
Key key,
|
||||||
this.initialSettings,
|
this.convID,
|
||||||
this.initialUsers,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -29,34 +41,82 @@ class UpdateConversationScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
||||||
|
Conversation _conversation;
|
||||||
|
|
||||||
TextEditingController _nameController = TextEditingController();
|
TextEditingController _nameController = TextEditingController();
|
||||||
|
TextEditingController _colorController = TextEditingController();
|
||||||
UsersList _members = UsersList();
|
UsersList _members = UsersList();
|
||||||
|
Set<int> _admins = Set();
|
||||||
bool _followConversation = true;
|
bool _followConversation = true;
|
||||||
bool _canEveryoneAddMembers = true;
|
bool _canEveryoneAddMembers = true;
|
||||||
|
String _image;
|
||||||
|
|
||||||
get isUpdating => widget.initialSettings != null;
|
String get _conversationColor => _colorController.text;
|
||||||
|
|
||||||
get isOwner => !isUpdating || widget.initialSettings.isOwner;
|
Color get _color {
|
||||||
|
if (_conversationColor == null || _conversationColor.isEmpty) return null;
|
||||||
|
|
||||||
Conversation get _initialSettings => widget.initialSettings;
|
try {
|
||||||
|
return HexColor(_conversationColor);
|
||||||
bool get _canAddMembers => isOwner || _initialSettings.canEveryoneAddMembers;
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
@override
|
return null;
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
// Check if we are updating an existing conversation
|
|
||||||
if (widget.initialSettings != null) {
|
|
||||||
_nameController.text = widget.initialSettings.name;
|
|
||||||
_members = widget.initialUsers;
|
|
||||||
_followConversation = widget.initialSettings.following;
|
|
||||||
_canEveryoneAddMembers = widget.initialSettings.canEveryoneAddMembers;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isUpdating => widget.convID != null;
|
||||||
|
|
||||||
|
get isAdmin => !isUpdating || _conversation.isAdmin;
|
||||||
|
|
||||||
|
bool get _canAddMembers =>
|
||||||
|
(isAdmin || _conversation.canEveryoneAddMembers) &&
|
||||||
|
(!isUpdating || !_conversation.isManaged);
|
||||||
|
|
||||||
|
get _isValid => _members.length > 0;
|
||||||
|
|
||||||
|
Future<void> _init() async {
|
||||||
|
if (!isUpdating) {
|
||||||
|
_admins.add(userID());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_conversation =
|
||||||
|
await ConversationsHelper().getSingle(widget.convID, force: true);
|
||||||
|
|
||||||
|
_nameController.text = _conversation.name ?? "";
|
||||||
|
_colorController.text = _conversation.color == null
|
||||||
|
? ""
|
||||||
|
: "#${colorToHex(_conversation.color)}";
|
||||||
|
_members = await UsersHelper().getList(_conversation.membersID);
|
||||||
|
_admins = _conversation.adminsID;
|
||||||
|
_followConversation = _conversation.following;
|
||||||
|
_canEveryoneAddMembers = _conversation.canEveryoneAddMembers;
|
||||||
|
_image = _conversation.logoURL;
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: ComunicBackButton(),
|
||||||
|
title: Text(isUpdating
|
||||||
|
? tr("Update conversation")
|
||||||
|
: tr("Create a conversation")),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.check),
|
||||||
|
onPressed: _isValid ? _submitForm : null)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: AsyncScreenWidget(
|
||||||
|
onReload: _init,
|
||||||
|
onBuild: _buildBody,
|
||||||
|
errorMessage: tr("Failed to load conversation settings!"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildBody() {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
@ -66,12 +126,27 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr("Conversation name (optionnal)"),
|
labelText: tr("Conversation name (optional)"),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
enabled: isOwner,
|
enabled: isAdmin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Conversation color
|
||||||
|
TextField(
|
||||||
|
controller: _colorController,
|
||||||
|
onChanged: (s) => setState(() {}),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: tr("Conversation color (optional)"),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
enabled: isAdmin,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(Icons.colorize),
|
||||||
|
color: _color,
|
||||||
|
onPressed: isAdmin ? _pickColor : null,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
|
||||||
// Follow conversation ?
|
// Follow conversation ?
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -90,7 +165,7 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Switch.adaptive(
|
Switch.adaptive(
|
||||||
value: _canEveryoneAddMembers,
|
value: _canEveryoneAddMembers,
|
||||||
onChanged: isOwner
|
onChanged: isAdmin
|
||||||
? (b) => setState(() {
|
? (b) => setState(() {
|
||||||
_canEveryoneAddMembers = b;
|
_canEveryoneAddMembers = b;
|
||||||
})
|
})
|
||||||
@ -106,98 +181,219 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
|||||||
keepFocusOnChoose: true,
|
keepFocusOnChoose: true,
|
||||||
label: tr("Add member"),
|
label: tr("Add member"),
|
||||||
enabled: _canAddMembers,
|
enabled: _canAddMembers,
|
||||||
onSelectUser: (user) => setState(() {
|
onSelectUser: (user) => _addMember(user)),
|
||||||
if (!_members.contains(user)) _members.insert(0, user);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
//Conversation members
|
//Conversation members
|
||||||
Column(
|
Column(
|
||||||
children: _members
|
children: _members.map((f) => _buildMemberTile(f)).toList(),
|
||||||
.map((f) => SimpleUserTile(
|
|
||||||
user: f,
|
|
||||||
trailing: _canAddMembers
|
|
||||||
? PopupMenuButton<_MembersMenuChoices>(
|
|
||||||
captureInheritedThemes: false,
|
|
||||||
onSelected: (choice) =>
|
|
||||||
_membersMenuItemSelected(f, choice),
|
|
||||||
itemBuilder: (c) =>
|
|
||||||
<PopupMenuEntry<_MembersMenuChoices>>[
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Text(tr("Remove")),
|
|
||||||
value: _MembersMenuChoices.REMOVE,
|
|
||||||
enabled: isOwner ||
|
|
||||||
(_canEveryoneAddMembers &&
|
|
||||||
!_initialSettings.members
|
|
||||||
.contains(f.id)),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Submit button
|
// Conversation image
|
||||||
RaisedButton(
|
isUpdating ? _buildConversationImageWidget() : Container(),
|
||||||
onPressed: _members.length < 1 ? null : _submitForm,
|
|
||||||
child: Text(isUpdating
|
|
||||||
? tr("Update the conversation")
|
|
||||||
: tr("Create the conversation")),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildMemberTile(User user) => SimpleUserTile(
|
||||||
|
user: user,
|
||||||
|
subtitle: _admins.contains(user.id) ? tr("Admin") : tr("Member"),
|
||||||
|
trailing: _canAddMembers
|
||||||
|
? PopupMenuButton<_MembersMenuChoices>(
|
||||||
|
onSelected: (choice) => _membersMenuItemSelected(user, choice),
|
||||||
|
itemBuilder: (c) => <PopupMenuEntry<_MembersMenuChoices>>[
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(tr("Toggle admin status")),
|
||||||
|
value: _MembersMenuChoices.TOGGLE_ADMIN_STATUS,
|
||||||
|
enabled: isUpdating && isAdmin && user.id != userID(),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(tr("Remove")),
|
||||||
|
value: _MembersMenuChoices.REMOVE,
|
||||||
|
enabled: isAdmin && user.id != userID(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
void _pickColor() async {
|
||||||
|
final color = await showColorPickerDialog(context, _color);
|
||||||
|
setState(() =>
|
||||||
|
_colorController.text = color == null ? "" : "#${colorToHex(color)}");
|
||||||
|
}
|
||||||
|
|
||||||
/// An option of the members menu has been selected
|
/// An option of the members menu has been selected
|
||||||
void _membersMenuItemSelected(User user, _MembersMenuChoices choice) {
|
void _membersMenuItemSelected(User user, _MembersMenuChoices choice) {
|
||||||
if (choice == null) return;
|
if (choice == null) return;
|
||||||
|
|
||||||
if (choice == _MembersMenuChoices.REMOVE)
|
switch (choice) {
|
||||||
|
case _MembersMenuChoices.REMOVE:
|
||||||
|
_removeMember(user);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _MembersMenuChoices.TOGGLE_ADMIN_STATUS:
|
||||||
|
_toggleAdminStatus(user);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addMember(User user) async {
|
||||||
|
try {
|
||||||
|
if (_members.contains(user)) return;
|
||||||
|
|
||||||
|
if (isUpdating)
|
||||||
|
await ConversationsHelper.addMember(_conversation.id, user.id);
|
||||||
|
|
||||||
|
setState(() => _members.insert(0, user));
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to add member to conversation!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeMember(User user) async {
|
||||||
|
try {
|
||||||
|
if (isUpdating)
|
||||||
|
await ConversationsHelper.removeMember(_conversation.id, user.id);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_members.removeWhere((u) => u.id == user.id);
|
_members.removeWhere((u) => u.id == user.id);
|
||||||
|
_admins.remove(user.id);
|
||||||
});
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to remove member!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleAdminStatus(User user) async {
|
||||||
|
try {
|
||||||
|
final setAdmin = !_admins.contains(user.id);
|
||||||
|
await ConversationsHelper.setAdmin(_conversation.id, user.id, setAdmin);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (!setAdmin)
|
||||||
|
_admins.remove(user.id);
|
||||||
|
else
|
||||||
|
_admins.add(user.id);
|
||||||
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to toggle admin status of user!"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Submit the conversation
|
/// Submit the conversation
|
||||||
Future<void> _submitForm() async {
|
Future<void> _submitForm() async {
|
||||||
final settings = Conversation(
|
try {
|
||||||
id: isUpdating ? widget.initialSettings.id : 0,
|
|
||||||
ownerID: isUpdating ? widget.initialSettings.ownerID : 0,
|
|
||||||
name: _nameController.text,
|
|
||||||
following: _followConversation,
|
|
||||||
members: _members.usersID,
|
|
||||||
canEveryoneAddMembers: _canEveryoneAddMembers,
|
|
||||||
|
|
||||||
// Give random value to these fields as they are ignored here
|
|
||||||
lastActive: 0,
|
|
||||||
sawLastMessage: true);
|
|
||||||
|
|
||||||
// Create the conversation
|
// Create the conversation
|
||||||
var conversationID = settings.id;
|
if (!isUpdating) {
|
||||||
bool error = false;
|
final conversationID = await ConversationsHelper.createConversation(
|
||||||
if (isUpdating)
|
NewConversation(
|
||||||
error = !(await ConversationsHelper().updateConversation(settings));
|
name: _nameController.text,
|
||||||
else {
|
members: _members.map((element) => element.id).toList(),
|
||||||
conversationID = await ConversationsHelper().createConversation(settings);
|
follow: _followConversation,
|
||||||
if (conversationID < 1) error = true;
|
canEveryoneAddMembers: _canEveryoneAddMembers,
|
||||||
}
|
color: _color));
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
if (error)
|
|
||||||
return Scaffold.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Text(isUpdating
|
|
||||||
? tr("Could not update the conversation!")
|
|
||||||
: tr("Could not create the conversation!")),
|
|
||||||
duration: Duration(seconds: 1),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Open the conversation
|
|
||||||
|
|
||||||
MainController.of(context).popPage();
|
MainController.of(context).popPage();
|
||||||
if (!isUpdating)
|
|
||||||
MainController.of(context).openConversation(conversationID);
|
MainController.of(context).openConversation(conversationID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update conversation settings
|
||||||
|
final newSettings = NewConversationsSettings(
|
||||||
|
convID: _conversation.id,
|
||||||
|
following: _followConversation,
|
||||||
|
isComplete: isAdmin,
|
||||||
|
name: _nameController.text,
|
||||||
|
canEveryoneAddMembers: _canEveryoneAddMembers,
|
||||||
|
color: _color,
|
||||||
|
);
|
||||||
|
|
||||||
|
await ConversationsHelper.updateConversation(newSettings);
|
||||||
|
|
||||||
|
MainController.of(context).popPage();
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to update conversation settings!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conversation image management
|
||||||
|
Widget _buildConversationImageWidget() => Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 10),
|
||||||
|
Text(tr("Conversation logo"),
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
_image == null
|
||||||
|
? Text("No logo defined yet.")
|
||||||
|
: CachedNetworkImage(imageUrl: _image),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
isAdmin
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: _uploadNewLogo,
|
||||||
|
child: Text(tr("Change logo")),
|
||||||
|
),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
_image == null
|
||||||
|
? Container()
|
||||||
|
: ElevatedButton(
|
||||||
|
onPressed: _deleteLogo,
|
||||||
|
child: Text(tr("Delete logo")),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty.all(Colors.red)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Upload new conversation logo
|
||||||
|
Future<void> _uploadNewLogo() async {
|
||||||
|
try {
|
||||||
|
final newLogo = await showPickFileDialog(
|
||||||
|
context: context,
|
||||||
|
allowedMimeTypes: ["image/png", "image/jpeg", "image/gif"],
|
||||||
|
imageMaxWidth: srvConfig.conversationsPolicy.maxLogoWidth,
|
||||||
|
imageMaxHeight: srvConfig.conversationsPolicy.maxLogoHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newLogo == null) return;
|
||||||
|
|
||||||
|
await ConversationsHelper.changeImage(_conversation.id, newLogo);
|
||||||
|
|
||||||
|
final newConvSettings =
|
||||||
|
await ConversationsHelper().getSingle(_conversation.id, force: true);
|
||||||
|
setState(() => _image = newConvSettings.logoURL);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to change conversation logo !"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete conversation logo
|
||||||
|
Future<void> _deleteLogo() async {
|
||||||
|
try {
|
||||||
|
if (!await showConfirmDialog(
|
||||||
|
context: context,
|
||||||
|
message: tr("Do you really want to delete this logo?"))) return;
|
||||||
|
|
||||||
|
await ConversationsHelper.removeLogo(_conversation.id);
|
||||||
|
|
||||||
|
setState(() => _image = null);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to remove conversation logo!"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ class _UserAccessDeniedScreenState extends SafeState<UserAccessDeniedScreen> {
|
|||||||
Widget _buildPage() {
|
Widget _buildPage() {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints.loose(size),
|
width: size.width,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
@ -99,7 +99,7 @@ class _UserPageScreenState extends SafeState<UserPageScreen> {
|
|||||||
body: Center(
|
body: Center(
|
||||||
child:
|
child:
|
||||||
buildErrorCard(tr("Could not get user information!"), actions: [
|
buildErrorCard(tr("Could not get user information!"), actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
onPressed: _getUserInfo,
|
onPressed: _getUserInfo,
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Retry").toUpperCase(),
|
tr("Retry").toUpperCase(),
|
||||||
|
@ -1,59 +1,90 @@
|
|||||||
import 'package:comunic/models/conversation_message.dart';
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
import 'package:comunic/models/user.dart';
|
import 'package:comunic/models/user.dart';
|
||||||
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
import 'package:comunic/ui/widgets/conversation_file_tile.dart';
|
||||||
import 'package:comunic/ui/widgets/network_image_widget.dart';
|
|
||||||
import 'package:comunic/ui/widgets/text_widget.dart';
|
import 'package:comunic/ui/widgets/text_widget.dart';
|
||||||
|
import 'package:comunic/utils/clipboard_utils.dart';
|
||||||
import 'package:comunic/utils/date_utils.dart';
|
import 'package:comunic/utils/date_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Conversation message tile
|
/// Conversation message tile
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
enum _MenuChoices { DELETE, REQUEST_UPDATE_CONTENT }
|
enum _MenuChoices {
|
||||||
|
COPY_URL,
|
||||||
|
COPY_MESSAGE,
|
||||||
|
DELETE,
|
||||||
|
REQUEST_UPDATE_CONTENT,
|
||||||
|
GET_STATS,
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef OnRequestMessageStats = void Function(ConversationMessage);
|
||||||
typedef OnRequestMessageUpdate = void Function(ConversationMessage);
|
typedef OnRequestMessageUpdate = void Function(ConversationMessage);
|
||||||
typedef OnRequestMessageDelete = void Function(ConversationMessage);
|
typedef OnRequestMessageDelete = void Function(ConversationMessage);
|
||||||
|
|
||||||
class ConversationMessageTile extends StatelessWidget {
|
class ConversationMessageTile extends StatelessWidget {
|
||||||
final ConversationMessage message;
|
final ConversationMessage message;
|
||||||
final User userInfo;
|
final User user;
|
||||||
final bool isLastMessage;
|
final OnRequestMessageStats onRequestMessageStats;
|
||||||
final bool isFirstMessage;
|
|
||||||
final OnRequestMessageUpdate onRequestMessageUpdate;
|
final OnRequestMessageUpdate onRequestMessageUpdate;
|
||||||
final OnRequestMessageDelete onRequestMessageDelete;
|
final OnRequestMessageDelete onRequestMessageDelete;
|
||||||
|
|
||||||
const ConversationMessageTile({
|
const ConversationMessageTile({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.message,
|
@required this.message,
|
||||||
@required this.userInfo,
|
@required this.user,
|
||||||
@required this.isLastMessage,
|
@required this.onRequestMessageStats,
|
||||||
@required this.isFirstMessage,
|
|
||||||
@required this.onRequestMessageUpdate,
|
@required this.onRequestMessageUpdate,
|
||||||
@required this.onRequestMessageDelete,
|
@required this.onRequestMessageDelete,
|
||||||
}) : assert(message != null),
|
}) : assert(message != null),
|
||||||
assert(userInfo != null),
|
assert(user != null),
|
||||||
assert(isLastMessage != null),
|
assert(onRequestMessageStats != null),
|
||||||
assert(isFirstMessage != null),
|
|
||||||
assert(onRequestMessageUpdate != null),
|
assert(onRequestMessageUpdate != null),
|
||||||
assert(onRequestMessageDelete != null),
|
assert(onRequestMessageDelete != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// Build account image
|
@override
|
||||||
Widget _buildAccountImage(BuildContext context) {
|
Widget build(BuildContext context) => Column(
|
||||||
return Container(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
margin: EdgeInsets.all(10.0),
|
children: <Widget>[
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text:
|
||||||
|
"${user.fullName} - ${formatDisplayDate(message.date, date: false)}",
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 11),
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
child: PopupMenuButton<_MenuChoices>(
|
child: PopupMenuButton<_MenuChoices>(
|
||||||
child: AccountImageWidget(
|
child: Icon(
|
||||||
user: userInfo,
|
Icons.more_vert,
|
||||||
width: 35.0,
|
color: Colors.white,
|
||||||
|
size: 14,
|
||||||
),
|
),
|
||||||
itemBuilder: (c) => [
|
onSelected: (v) => _menuOptionSelected(context, v),
|
||||||
|
itemBuilder: (c) => <PopupMenuItem<_MenuChoices>>[
|
||||||
|
PopupMenuItem(
|
||||||
|
enabled: (message.message?.content ?? "") != "",
|
||||||
|
value: _MenuChoices.COPY_MESSAGE,
|
||||||
|
child: Text(tr("Copy message")),
|
||||||
|
),
|
||||||
|
|
||||||
|
PopupMenuItem(
|
||||||
|
enabled: message.file != null,
|
||||||
|
value: _MenuChoices.COPY_URL,
|
||||||
|
child: Text(tr("Copy URL")),
|
||||||
|
),
|
||||||
|
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _MenuChoices.GET_STATS,
|
||||||
|
child: Text(tr("Statistics")),
|
||||||
|
),
|
||||||
|
|
||||||
// Update message content
|
// Update message content
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: message.isOwner,
|
enabled: message.isOwner &&
|
||||||
|
message.message != null &&
|
||||||
|
message.message.content.isNotEmpty,
|
||||||
value: _MenuChoices.REQUEST_UPDATE_CONTENT,
|
value: _MenuChoices.REQUEST_UPDATE_CONTENT,
|
||||||
child: Text(tr("Update")),
|
child: Text(tr("Update")),
|
||||||
),
|
),
|
||||||
@ -64,190 +95,41 @@ class ConversationMessageTile extends StatelessWidget {
|
|||||||
value: _MenuChoices.DELETE,
|
value: _MenuChoices.DELETE,
|
||||||
child: Text(tr("Delete")),
|
child: Text(tr("Delete")),
|
||||||
),
|
),
|
||||||
],
|
]..removeWhere((element) => !element.enabled),
|
||||||
onSelected: _menuOptionSelected,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build widget image
|
|
||||||
Widget _buildMessageImage(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(bottom: 2),
|
|
||||||
child: NetworkImageWidget(
|
|
||||||
url: message.imageURL,
|
|
||||||
allowFullScreen: true,
|
|
||||||
width: 200,
|
|
||||||
height: 200,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build message date
|
|
||||||
Widget _buildMessageDate() {
|
|
||||||
return isLastMessage
|
|
||||||
? Container(
|
|
||||||
margin: EdgeInsets.only(top: 5.0),
|
|
||||||
child: Text(
|
|
||||||
dateTimeToString(message.date),
|
|
||||||
style: TextStyle(color: Colors.black54, fontSize: 12.0),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container();
|
]),
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a message of the current user
|
|
||||||
Widget _buildRightMessage(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(bottom: isLastMessage ? 20.0 : 2.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
// Text image
|
|
||||||
Container(
|
|
||||||
child: message.hasImage
|
|
||||||
? _buildMessageImage(context)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
|
_buildMessageContent(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Text message
|
Widget _buildMessageContent() {
|
||||||
Container(
|
if (!message.hasFile)
|
||||||
child: message.hasMessage
|
return TextWidget(
|
||||||
? Container(
|
|
||||||
width: 200.0,
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Container(
|
|
||||||
child: TextWidget(
|
|
||||||
content: message.message,
|
content: message.message,
|
||||||
textAlign: TextAlign.justify,
|
textAlign: TextAlign.justify,
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white),
|
||||||
linksColor: Colors.indigo,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
15.0, 10.0, 15.0, 10.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Account image
|
|
||||||
_buildAccountImage(context)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Date
|
|
||||||
Container(
|
|
||||||
child: _buildMessageDate(),
|
|
||||||
margin: EdgeInsets.only(right: 45.0),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
// Text
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a message of a peer user
|
return ConversationFileWidget(messageID: message.id, file: message.file);
|
||||||
Widget _buildLeftMessage(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(bottom: isLastMessage ? 20.0 : 5.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
//User name
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(left: 55.0),
|
|
||||||
child: isFirstMessage
|
|
||||||
? Text(
|
|
||||||
userInfo.fullName,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: TextStyle(fontSize: 12.0),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
// Account image
|
|
||||||
_buildAccountImage(context),
|
|
||||||
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
// Text image
|
|
||||||
Container(
|
|
||||||
child:
|
|
||||||
message.hasImage ? _buildMessageImage(context) : null,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Text message
|
|
||||||
Container(
|
|
||||||
child: message.hasMessage
|
|
||||||
? Container(
|
|
||||||
width: 200.0,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Container(
|
|
||||||
child: TextWidget(
|
|
||||||
content: message.message,
|
|
||||||
textAlign: TextAlign.justify,
|
|
||||||
style: TextStyle(
|
|
||||||
color: darkTheme()
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black),
|
|
||||||
),
|
|
||||||
padding:
|
|
||||||
EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 10.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: darkTheme()
|
|
||||||
? Colors.white12
|
|
||||||
: Colors.black12,
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// Date
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(left: 50.0),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: _buildMessageDate(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return message.isOwner
|
|
||||||
? _buildRightMessage(context)
|
|
||||||
: _buildLeftMessage(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process menu choice
|
/// Process menu choice
|
||||||
void _menuOptionSelected(_MenuChoices value) {
|
void _menuOptionSelected(BuildContext context, _MenuChoices value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
case _MenuChoices.COPY_MESSAGE:
|
||||||
|
copyToClipboard(context, message.message.content);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _MenuChoices.COPY_URL:
|
||||||
|
copyToClipboard(context, message.message.content);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _MenuChoices.GET_STATS:
|
||||||
|
onRequestMessageStats(message);
|
||||||
|
break;
|
||||||
|
|
||||||
case _MenuChoices.REQUEST_UPDATE_CONTENT:
|
case _MenuChoices.REQUEST_UPDATE_CONTENT:
|
||||||
onRequestMessageUpdate(message);
|
onRequestMessageUpdate(message);
|
||||||
break;
|
break;
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import 'package:comunic/helpers/conversations_helper.dart';
|
import 'package:comunic/helpers/conversations_helper.dart';
|
||||||
import 'package:comunic/lists/users_list.dart';
|
import 'package:comunic/lists/users_list.dart';
|
||||||
import 'package:comunic/models/conversation.dart';
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||||
|
import 'package:comunic/ui/widgets/conversation_image_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/custom_list_tile.dart';
|
import 'package:comunic/ui/widgets/custom_list_tile.dart';
|
||||||
import 'package:comunic/utils/date_utils.dart';
|
import 'package:comunic/utils/date_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Single conversation tile
|
/// Single conversation tile
|
||||||
@ -12,17 +13,17 @@ import 'package:flutter/material.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
typedef OpenConversationCallback = void Function(Conversation);
|
typedef OpenConversationCallback = void Function(Conversation);
|
||||||
typedef RequestDeleteConversationCallback = void Function(Conversation);
|
typedef RequestLeaveConversationCallback = void Function(Conversation);
|
||||||
typedef RequestUpdateConversationCallback = void Function(Conversation);
|
typedef RequestUpdateConversationCallback = void Function(Conversation);
|
||||||
|
|
||||||
enum _PopupMenuChoices { UPDATE, DELETE }
|
enum _PopupMenuChoices { UPDATE, LEAVE }
|
||||||
|
|
||||||
class ConversationTile extends StatelessWidget {
|
class ConversationTile extends StatelessWidget {
|
||||||
final Conversation conversation;
|
final Conversation conversation;
|
||||||
final UsersList usersList;
|
final UsersList usersList;
|
||||||
final OpenConversationCallback onOpen;
|
final OpenConversationCallback onOpen;
|
||||||
final RequestUpdateConversationCallback onRequestUpdate;
|
final RequestUpdateConversationCallback onRequestUpdate;
|
||||||
final RequestDeleteConversationCallback onRequestDelete;
|
final RequestLeaveConversationCallback onRequestLeave;
|
||||||
|
|
||||||
const ConversationTile({
|
const ConversationTile({
|
||||||
Key key,
|
Key key,
|
||||||
@ -30,12 +31,12 @@ class ConversationTile extends StatelessWidget {
|
|||||||
@required this.usersList,
|
@required this.usersList,
|
||||||
@required this.onOpen,
|
@required this.onOpen,
|
||||||
@required this.onRequestUpdate,
|
@required this.onRequestUpdate,
|
||||||
@required this.onRequestDelete,
|
@required this.onRequestLeave,
|
||||||
}) : assert(conversation != null),
|
}) : assert(conversation != null),
|
||||||
assert(usersList != null),
|
assert(usersList != null),
|
||||||
assert(onOpen != null),
|
assert(onOpen != null),
|
||||||
assert(onRequestUpdate != null),
|
assert(onRequestUpdate != null),
|
||||||
assert(onRequestDelete != null),
|
assert(onRequestLeave != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
_buildSubInformation(IconData icon, String content) {
|
_buildSubInformation(IconData icon, String content) {
|
||||||
@ -52,8 +53,11 @@ class ConversationTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Column(
|
||||||
return CustomListTile(
|
children: [_buildMainTile(context), _buildCallTile(context)],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildMainTile(BuildContext context) => CustomListTile(
|
||||||
onTap: () => onOpen(conversation),
|
onTap: () => onOpen(conversation),
|
||||||
// Conversation name
|
// Conversation name
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -66,21 +70,22 @@ class ConversationTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Tile color
|
||||||
|
tileColor: conversation.sawLastMessage
|
||||||
|
? null
|
||||||
|
: (conversation.color ?? Colors.blue).withOpacity(0.2),
|
||||||
|
|
||||||
// Leading icon
|
// Leading icon
|
||||||
leading: Icon(
|
leading: ConversationImageWidget(
|
||||||
conversation.sawLastMessage ? Icons.check_circle : Icons.lens,
|
conversation: conversation, users: usersList),
|
||||||
color: conversation.sawLastMessage
|
|
||||||
? (darkTheme() ? darkAccentColor : null)
|
|
||||||
: Colors.blue,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Conversation information
|
// Conversation information
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildSubInformation(
|
_buildSubInformation(Icons.access_time,
|
||||||
Icons.access_time, diffTimeFromNowToStr(conversation.lastActive)),
|
diffTimeFromNowToStr(conversation.lastActivity)),
|
||||||
_buildSubInformation(
|
_buildSubInformation(
|
||||||
Icons.group,
|
Icons.group,
|
||||||
conversation.members.length == 1
|
conversation.members.length == 1
|
||||||
@ -105,12 +110,27 @@ class ConversationTile extends StatelessWidget {
|
|||||||
value: _PopupMenuChoices.UPDATE,
|
value: _PopupMenuChoices.UPDATE,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Text(tr("Delete")),
|
child: Text(tr("Leave")),
|
||||||
value: _PopupMenuChoices.DELETE,
|
value: _PopupMenuChoices.LEAVE,
|
||||||
)
|
)
|
||||||
]).then(_conversationMenuCallback);
|
]).then(_conversationMenuCallback);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Build call tile, in case of ongoing call
|
||||||
|
Widget _buildCallTile(BuildContext context) {
|
||||||
|
if (!conversation.isHavingCall) return Container();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 20),
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () => MainController.of(context).startCall(conversation.id),
|
||||||
|
dense: true,
|
||||||
|
title: Text(tr("Ongoing call")),
|
||||||
|
leading: Icon(Icons.call),
|
||||||
|
tileColor: Colors.yellow.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Method called each time an option of the menu is selected
|
/// Method called each time an option of the menu is selected
|
||||||
@ -120,8 +140,8 @@ class ConversationTile extends StatelessWidget {
|
|||||||
onRequestUpdate(conversation);
|
onRequestUpdate(conversation);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case _PopupMenuChoices.DELETE:
|
case _PopupMenuChoices.LEAVE:
|
||||||
onRequestDelete(conversation);
|
onRequestLeave(conversation);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,21 +39,25 @@ class PendingFriendTile extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FlatButton(
|
ElevatedButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Accept").toUpperCase(),
|
tr("Accept").toUpperCase(),
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
color: Colors.green,
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStateProperty.all(Colors.green)),
|
||||||
onPressed: () => onRespond(friend, true),
|
onPressed: () => onRespond(friend, true),
|
||||||
),
|
),
|
||||||
Container(width: 8.0,),
|
Container(
|
||||||
FlatButton(
|
width: 8.0,
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Reject").toUpperCase(),
|
tr("Reject").toUpperCase(),
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
color: Colors.red,
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStateProperty.all(Colors.red)),
|
||||||
onPressed: () => onRespond(friend, false),
|
onPressed: () => onRespond(friend, false),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -25,9 +25,12 @@ import 'package:comunic/utils/navigation_utils.dart';
|
|||||||
import 'package:comunic/utils/post_utils.dart';
|
import 'package:comunic/utils/post_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../../models/api_request.dart';
|
||||||
|
import '../../utils/log_utils.dart';
|
||||||
|
|
||||||
/// Single posts tile
|
/// Single posts tile
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
@ -76,7 +79,7 @@ class _PostTileState extends State<PostTile> {
|
|||||||
|
|
||||||
// Class members
|
// Class members
|
||||||
TextEditingController _commentController = TextEditingController();
|
TextEditingController _commentController = TextEditingController();
|
||||||
PickedFile _commentImage;
|
BytesFile _commentImage;
|
||||||
bool _submitting = false;
|
bool _submitting = false;
|
||||||
int _maxNumberOfCommentToShow = 10;
|
int _maxNumberOfCommentToShow = 10;
|
||||||
|
|
||||||
@ -264,9 +267,9 @@ class _PostTileState extends State<PostTile> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPostYouTube() {
|
Widget _buildPostYouTube() {
|
||||||
return RaisedButton(
|
return ElevatedButton(
|
||||||
color: Colors.red,
|
style:
|
||||||
textColor: Colors.white,
|
ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.red)),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -321,7 +324,7 @@ class _PostTileState extends State<PostTile> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPostPDF() {
|
Widget _buildPostPDF() {
|
||||||
return RaisedButton.icon(
|
return ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launch(widget.post.fileURL);
|
launch(widget.post.fileURL);
|
||||||
},
|
},
|
||||||
@ -442,8 +445,7 @@ class _PostTileState extends State<PostTile> {
|
|||||||
// Image button
|
// Image button
|
||||||
Container(
|
Container(
|
||||||
width: 30,
|
width: 30,
|
||||||
child: FlatButton(
|
child: TextButton(
|
||||||
padding: EdgeInsets.only(),
|
|
||||||
onPressed: _pickImageForComment,
|
onPressed: _pickImageForComment,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.image,
|
Icons.image,
|
||||||
@ -455,8 +457,7 @@ class _PostTileState extends State<PostTile> {
|
|||||||
// Submit button
|
// Submit button
|
||||||
Container(
|
Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
child: FlatButton(
|
child: TextButton(
|
||||||
padding: EdgeInsets.only(),
|
|
||||||
onPressed: _canSubmitComment ? () => _submitComment() : null,
|
onPressed: _canSubmitComment ? () => _submitComment() : null,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.send,
|
Icons.send,
|
||||||
@ -494,11 +495,16 @@ class _PostTileState extends State<PostTile> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// Pick a new image
|
// Pick a new image
|
||||||
final newImage = await pickImage(context);
|
final newImage = await pickImage(context);
|
||||||
setState(() {
|
setState(() {
|
||||||
_commentImage = newImage;
|
_commentImage = newImage;
|
||||||
});
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to choose an image!"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Submit comment entered by the user
|
/// Submit comment entered by the user
|
||||||
|
33
lib/ui/tiles/server_conversation_message_tile.dart
Normal file
33
lib/ui/tiles/server_conversation_message_tile.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:comunic/lists/users_list.dart';
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Server conversation message list
|
||||||
|
///
|
||||||
|
/// @author Pierre hubert
|
||||||
|
|
||||||
|
class ServerConversationMessageTile extends StatelessWidget {
|
||||||
|
final ConversationServerMessage message;
|
||||||
|
final UsersList users;
|
||||||
|
|
||||||
|
const ServerConversationMessageTile({
|
||||||
|
Key key,
|
||||||
|
@required this.message,
|
||||||
|
@required this.users,
|
||||||
|
}) : assert(message != null),
|
||||||
|
assert(users != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
message.getText(users),
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,15 @@ class SimpleUserTile extends StatelessWidget {
|
|||||||
final User user;
|
final User user;
|
||||||
final OnUserTap onTap;
|
final OnUserTap onTap;
|
||||||
final Widget trailing;
|
final Widget trailing;
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
const SimpleUserTile({Key key, this.user, this.onTap, this.trailing})
|
const SimpleUserTile({
|
||||||
: assert(user != null),
|
Key key,
|
||||||
|
this.user,
|
||||||
|
this.onTap,
|
||||||
|
this.trailing,
|
||||||
|
this.subtitle,
|
||||||
|
}) : assert(user != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -27,6 +33,7 @@ class SimpleUserTile extends StatelessWidget {
|
|||||||
user: user,
|
user: user,
|
||||||
),
|
),
|
||||||
title: Text(user.fullName),
|
title: Text(user.fullName),
|
||||||
|
subtitle: subtitle == null ? null : Text(subtitle),
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ class _FriendshipStatusWidgetState extends State<FriendshipStatusWidget> {
|
|||||||
|
|
||||||
// No request sent yet
|
// No request sent yet
|
||||||
if (widget.status.noRequestExchanged) {
|
if (widget.status.noRequestExchanged) {
|
||||||
return RaisedButton(
|
return ElevatedButton(
|
||||||
child: Text(tr("Send request").toUpperCase()),
|
child: Text(tr("Send request").toUpperCase()),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
executeRequest(() => _friendsHelper.sendRequest(friendID)),
|
executeRequest(() => _friendsHelper.sendRequest(friendID)),
|
||||||
@ -56,12 +56,13 @@ class _FriendshipStatusWidgetState extends State<FriendshipStatusWidget> {
|
|||||||
|
|
||||||
// Already sent a friendship request
|
// Already sent a friendship request
|
||||||
if (widget.status.sentRequest) {
|
if (widget.status.sentRequest) {
|
||||||
return RaisedButton(
|
return ElevatedButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Cancel request").toUpperCase(),
|
tr("Cancel request").toUpperCase(),
|
||||||
style: WhiteTextColorStyle,
|
style: WhiteTextColorStyle,
|
||||||
),
|
),
|
||||||
color: Colors.red,
|
style:
|
||||||
|
ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.red)),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
executeRequest(() => _friendsHelper.cancelRequest(friendID)),
|
executeRequest(() => _friendsHelper.cancelRequest(friendID)),
|
||||||
);
|
);
|
||||||
@ -71,21 +72,23 @@ class _FriendshipStatusWidgetState extends State<FriendshipStatusWidget> {
|
|||||||
if (widget.status.receivedRequest) {
|
if (widget.status.receivedRequest) {
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
RaisedButton(
|
ElevatedButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Accept request").toUpperCase(),
|
tr("Accept request").toUpperCase(),
|
||||||
style: WhiteTextColorStyle,
|
style: WhiteTextColorStyle,
|
||||||
),
|
),
|
||||||
color: Colors.green,
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStateProperty.all(Colors.green)),
|
||||||
onPressed: () => executeRequest(
|
onPressed: () => executeRequest(
|
||||||
() => _friendsHelper.respondRequest(friendID, true)),
|
() => _friendsHelper.respondRequest(friendID, true)),
|
||||||
),
|
),
|
||||||
RaisedButton(
|
ElevatedButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr("Reject request").toUpperCase(),
|
tr("Reject request").toUpperCase(),
|
||||||
style: WhiteTextColorStyle,
|
style: WhiteTextColorStyle,
|
||||||
),
|
),
|
||||||
color: Colors.red,
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStateProperty.all(Colors.red)),
|
||||||
onPressed: () => executeRequest(
|
onPressed: () => executeRequest(
|
||||||
() => _friendsHelper.respondRequest(friendID, false)),
|
() => _friendsHelper.respondRequest(friendID, false)),
|
||||||
)
|
)
|
||||||
@ -94,7 +97,7 @@ class _FriendshipStatusWidgetState extends State<FriendshipStatusWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The two users are friends, offers to follow him
|
// The two users are friends, offers to follow him
|
||||||
return RaisedButton(
|
return ElevatedButton(
|
||||||
child: Text((widget.status.following ? tr("Following") : tr("Follow"))
|
child: Text((widget.status.following ? tr("Following") : tr("Follow"))
|
||||||
.toUpperCase()),
|
.toUpperCase()),
|
||||||
onPressed: () => executeRequest(() =>
|
onPressed: () => executeRequest(() =>
|
||||||
|
126
lib/ui/widgets/conversation_file_tile.dart
Normal file
126
lib/ui/widgets/conversation_file_tile.dart
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
/// Chat file tile
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
import 'package:comunic/ui/dialogs/audio_player_dialog.dart';
|
||||||
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||||
|
import 'package:comunic/ui/routes/video_player_route.dart';
|
||||||
|
import 'package:comunic/ui/widgets/network_image_widget.dart';
|
||||||
|
import 'package:filesize/filesize.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
const _AreaWidth = 150.0;
|
||||||
|
const _AreaHeight = 100.0;
|
||||||
|
|
||||||
|
class ConversationFileWidget extends StatefulWidget {
|
||||||
|
final int messageID;
|
||||||
|
final ConversationMessageFile file;
|
||||||
|
|
||||||
|
const ConversationFileWidget({
|
||||||
|
Key key,
|
||||||
|
@required this.messageID,
|
||||||
|
@required this.file,
|
||||||
|
}) : assert(messageID != null),
|
||||||
|
assert(file != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ConversationFileWidgetState createState() => _ConversationFileWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConversationFileWidgetState extends State<ConversationFileWidget> {
|
||||||
|
ConversationMessageFile get file => widget.file;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Stack(
|
||||||
|
children: [
|
||||||
|
!file.hasThumbnail ||
|
||||||
|
file.fileType == ConversationMessageFileType.IMAGE
|
||||||
|
? Container(
|
||||||
|
width: 0,
|
||||||
|
)
|
||||||
|
: Opacity(
|
||||||
|
opacity: 0.8,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: file.thumbnail,
|
||||||
|
width: _AreaWidth,
|
||||||
|
height: _AreaHeight,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: _AreaWidth,
|
||||||
|
height: _AreaHeight,
|
||||||
|
child: _buildContent(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
switch (file.fileType) {
|
||||||
|
// Images
|
||||||
|
case ConversationMessageFileType.IMAGE:
|
||||||
|
return Center(
|
||||||
|
child: NetworkImageWidget(
|
||||||
|
url: file.url,
|
||||||
|
thumbnailURL: file.thumbnail,
|
||||||
|
allowFullScreen: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We open it in the browser
|
||||||
|
default:
|
||||||
|
return Container(
|
||||||
|
child: Center(
|
||||||
|
child: MaterialButton(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Spacer(flex: 2),
|
||||||
|
Icon(file.icon, color: Colors.white),
|
||||||
|
Spacer(),
|
||||||
|
Text(
|
||||||
|
file.name.length < 23
|
||||||
|
? file.name
|
||||||
|
: file.name.substring(0, 20) + "...",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
Text(
|
||||||
|
filesize(file.size),
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.white),
|
||||||
|
),
|
||||||
|
Spacer(flex: 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: _openFile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openFile() {
|
||||||
|
switch (file.fileType) {
|
||||||
|
case ConversationMessageFileType.AUDIO:
|
||||||
|
showAudioPlayerDialog(context, file.url);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConversationMessageFileType.VIDEO:
|
||||||
|
MainController.of(context).push(
|
||||||
|
VideoPlayerRoute(url: file.url),
|
||||||
|
hideNavBar: true,
|
||||||
|
canShowAsDialog: true,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
launch(file.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
118
lib/ui/widgets/conversation_image_widget.dart
Normal file
118
lib/ui/widgets/conversation_image_widget.dart
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:comunic/lists/users_list.dart';
|
||||||
|
import 'package:comunic/models/conversation.dart';
|
||||||
|
import 'package:comunic/models/user.dart';
|
||||||
|
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Conversation image widget
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class ConversationImageWidget extends StatelessWidget {
|
||||||
|
final Conversation conversation;
|
||||||
|
final UsersList users;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
const ConversationImageWidget(
|
||||||
|
{Key key,
|
||||||
|
@required this.conversation,
|
||||||
|
@required this.users,
|
||||||
|
this.size = 30})
|
||||||
|
: assert(conversation != null),
|
||||||
|
assert(users != null),
|
||||||
|
assert(size > 0),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Material(
|
||||||
|
child: _buildIcon(),
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(18.0),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildIcon() {
|
||||||
|
if (conversation.logoURL != null)
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: conversation.logoURL,
|
||||||
|
width: size,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conversation.members.length < 2)
|
||||||
|
return Icon(
|
||||||
|
Icons.lock,
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conversation.members.length == 2)
|
||||||
|
return AccountImageWidget(
|
||||||
|
width: size,
|
||||||
|
user: users.getUser(conversation.otherMembersID.first),
|
||||||
|
);
|
||||||
|
|
||||||
|
return MultipleAccountImagesWidget(
|
||||||
|
users:
|
||||||
|
conversation.otherMembersID.map((id) => users.getUser(id)).toList(),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultipleAccountImagesWidget extends StatelessWidget {
|
||||||
|
final List<User> users;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
const MultipleAccountImagesWidget({
|
||||||
|
Key key,
|
||||||
|
@required this.users,
|
||||||
|
@required this.size,
|
||||||
|
}) : assert(users != null),
|
||||||
|
assert(size != null),
|
||||||
|
assert(size > 0),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: _buildContent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
if (users.length == 2) return _buildFirstRow();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [_buildFirstRow(), _buildSecondRow()],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFirstRow() => Row(
|
||||||
|
children: [
|
||||||
|
AccountImageWidget(
|
||||||
|
user: users[0],
|
||||||
|
width: size / 2,
|
||||||
|
),
|
||||||
|
AccountImageWidget(
|
||||||
|
user: users[1],
|
||||||
|
width: size / 2,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSecondRow() => Row(
|
||||||
|
children: [
|
||||||
|
AccountImageWidget(
|
||||||
|
user: users[2],
|
||||||
|
width: size / 2,
|
||||||
|
),
|
||||||
|
users.length > 3
|
||||||
|
? AccountImageWidget(
|
||||||
|
user: users[3],
|
||||||
|
width: size / 2,
|
||||||
|
)
|
||||||
|
: Container()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
@ -17,6 +17,7 @@ class CustomListTile extends StatelessWidget {
|
|||||||
final GestureTapCallback onTap;
|
final GestureTapCallback onTap;
|
||||||
final GestureLongPressCallback onLongPress;
|
final GestureLongPressCallback onLongPress;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
final Color tileColor;
|
||||||
|
|
||||||
/// Custom onLongPress function
|
/// Custom onLongPress function
|
||||||
final Function(Size, Offset) onLongPressWithInfo;
|
final Function(Size, Offset) onLongPressWithInfo;
|
||||||
@ -39,6 +40,7 @@ class CustomListTile extends StatelessWidget {
|
|||||||
this.selected = false,
|
this.selected = false,
|
||||||
this.onLongPressWithInfo,
|
this.onLongPressWithInfo,
|
||||||
this.onLongPressOpenMenu,
|
this.onLongPressOpenMenu,
|
||||||
|
this.tileColor,
|
||||||
}) : assert(isThreeLine != null),
|
}) : assert(isThreeLine != null),
|
||||||
assert(enabled != null),
|
assert(enabled != null),
|
||||||
assert(selected != null),
|
assert(selected != null),
|
||||||
@ -48,6 +50,7 @@ class CustomListTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
tileColor: tileColor,
|
||||||
leading: leading,
|
leading: leading,
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
|
@ -153,8 +153,7 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
),
|
),
|
||||||
RaisedButton(
|
ElevatedButton(
|
||||||
color: Colors.indigo,
|
|
||||||
onPressed: () => _tryConnect(),
|
onPressed: () => _tryConnect(),
|
||||||
child: Text(tr("Try again")),
|
child: Text(tr("Try again")),
|
||||||
),
|
),
|
||||||
|
@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class NetworkImageWidget extends StatelessWidget {
|
class NetworkImageWidget extends StatelessWidget {
|
||||||
final String url;
|
final String url;
|
||||||
|
final String thumbnailURL;
|
||||||
final bool allowFullScreen;
|
final bool allowFullScreen;
|
||||||
final bool roundedEdges;
|
final bool roundedEdges;
|
||||||
final double width;
|
final double width;
|
||||||
@ -19,6 +20,7 @@ class NetworkImageWidget extends StatelessWidget {
|
|||||||
const NetworkImageWidget({
|
const NetworkImageWidget({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.url,
|
@required this.url,
|
||||||
|
this.thumbnailURL,
|
||||||
this.allowFullScreen = false,
|
this.allowFullScreen = false,
|
||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
@ -42,7 +44,7 @@ class NetworkImageWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: url,
|
imageUrl: thumbnailURL ?? url,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import 'package:comunic/enums/post_kind.dart';
|
import 'package:comunic/enums/post_kind.dart';
|
||||||
import 'package:comunic/enums/post_target.dart';
|
import 'package:comunic/enums/post_target.dart';
|
||||||
import 'package:comunic/enums/post_visibility_level.dart';
|
import 'package:comunic/enums/post_visibility_level.dart';
|
||||||
@ -12,18 +11,19 @@ import 'package:comunic/utils/files_utils.dart';
|
|||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:comunic/utils/post_utils.dart';
|
import 'package:comunic/utils/post_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
import 'package:file_picker_cross/file_picker_cross.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
|
import '../../models/api_request.dart';
|
||||||
|
import '../../utils/log_utils.dart';
|
||||||
|
import '../../utils/ui_utils.dart';
|
||||||
|
|
||||||
/// Widget that allows to create posts
|
/// Widget that allows to create posts
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
const _ActiveButtonsColor = Colors.blue;
|
const _ActiveButtonsColor = Colors.blue;
|
||||||
const _ActiveButtonsTextColor = Colors.white;
|
|
||||||
const _InactiveButtonsColor = Colors.grey;
|
const _InactiveButtonsColor = Colors.grey;
|
||||||
const _InactiveButtonsTextColor = Colors.black;
|
|
||||||
|
|
||||||
class PostCreateFormWidget extends StatefulWidget {
|
class PostCreateFormWidget extends StatefulWidget {
|
||||||
final PostTarget postTarget;
|
final PostTarget postTarget;
|
||||||
@ -51,7 +51,7 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
|||||||
bool _isCreating = false;
|
bool _isCreating = false;
|
||||||
final TextEditingController _postTextController = TextEditingController();
|
final TextEditingController _postTextController = TextEditingController();
|
||||||
PostVisibilityLevel _postVisibilityLevel;
|
PostVisibilityLevel _postVisibilityLevel;
|
||||||
PickedFile _postImage;
|
BytesFile _postImage;
|
||||||
String _postURL;
|
String _postURL;
|
||||||
List<int> _postPDF;
|
List<int> _postPDF;
|
||||||
DateTime _timeEnd;
|
DateTime _timeEnd;
|
||||||
@ -193,14 +193,9 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
|||||||
// Submit post button
|
// Submit post button
|
||||||
_isCreating
|
_isCreating
|
||||||
? Container()
|
? Container()
|
||||||
: FlatButton(
|
: ElevatedButton(
|
||||||
child: Text(tr("Send").toUpperCase()),
|
child: Text(tr("Send").toUpperCase()),
|
||||||
onPressed: canSubmitForm ? _submitForm : null,
|
onPressed: canSubmitForm ? _submitForm : null),
|
||||||
color: _ActiveButtonsColor,
|
|
||||||
textColor: _ActiveButtonsTextColor,
|
|
||||||
disabledColor: _InactiveButtonsColor,
|
|
||||||
disabledTextColor: _InactiveButtonsTextColor,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -247,6 +242,7 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
|||||||
|
|
||||||
/// Pick an image for the new post
|
/// Pick an image for the new post
|
||||||
Future<void> _pickImageForPost() async {
|
Future<void> _pickImageForPost() async {
|
||||||
|
try {
|
||||||
final image = await pickImage(context);
|
final image = await pickImage(context);
|
||||||
|
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
@ -256,6 +252,10 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
this._postImage = image;
|
this._postImage = image;
|
||||||
});
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
snack(context, tr("Failed to pick an image for the post!"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Choose a new URL for the post
|
/// Choose a new URL for the post
|
||||||
@ -278,15 +278,18 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
|||||||
/// Pick a PDF for the new post
|
/// Pick a PDF for the new post
|
||||||
Future<void> _pickPDFForPost() async {
|
Future<void> _pickPDFForPost() async {
|
||||||
try {
|
try {
|
||||||
final picker = await FilePickerCross.importFromStorage(
|
final file = await FilePicker.platform.pickFiles(
|
||||||
type: FileTypeCross.custom,
|
type: FileType.custom,
|
||||||
fileExtension: "pdf",
|
allowedExtensions: ["pdf"],
|
||||||
|
withData: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (file == null || file.files.isEmpty) return;
|
||||||
|
|
||||||
_resetPostSelection();
|
_resetPostSelection();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
this._postPDF = picker.toUint8List();
|
this._postPDF = file.files.first.bytes;
|
||||||
});
|
});
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
print("Pick PDF error: $e\n$stack");
|
print("Pick PDF error: $e\n$stack");
|
||||||
|
@ -119,6 +119,7 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
|
|||||||
|
|
||||||
_loading = true;
|
_loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
final list = !getOlder
|
final list = !getOlder
|
||||||
? await widget.getPostsList()
|
? await widget.getPostsList()
|
||||||
: await widget.getOlder(_list.oldestID);
|
: await widget.getOlder(_list.oldestID);
|
||||||
@ -127,8 +128,6 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
|
|||||||
|
|
||||||
final users = await _usersHelper.getList(list.usersID);
|
final users = await _usersHelper.getList(list.usersID);
|
||||||
|
|
||||||
if (users == null) return _loadError();
|
|
||||||
|
|
||||||
final groups = await _groupsHelper.getList(list.groupsID);
|
final groups = await _groupsHelper.getList(list.groupsID);
|
||||||
|
|
||||||
if (groups == null) return _loadError();
|
if (groups == null) return _loadError();
|
||||||
@ -146,6 +145,10 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
|
|||||||
_groups.addAll(groups);
|
_groups.addAll(groups);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (e, s) {
|
||||||
|
print("Failed to load post information ! $e => $s");
|
||||||
|
_loadError();
|
||||||
|
}
|
||||||
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@ import 'package:flutter/material.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
abstract class SafeState<T extends StatefulWidget> extends State<T> {
|
abstract class SafeState<T extends StatefulWidget> extends State<T> {
|
||||||
final _subscriptions = List<StreamSubscription>();
|
final _subscriptions = <StreamSubscription>[];
|
||||||
final _timers = List<Timer>();
|
final _timers = <Timer>[];
|
||||||
|
|
||||||
bool _unmounted = false;
|
bool _unmounted = false;
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ class _ConversationWindowState extends SafeState<ConversationWindow> {
|
|||||||
_refresh();
|
_refresh();
|
||||||
|
|
||||||
listen<NewConversationMessageEvent>((e) {
|
listen<NewConversationMessageEvent>((e) {
|
||||||
if (e.msg.conversationID == _convID &&
|
if (e.msg.convID == _convID &&
|
||||||
_collapsed &&
|
_collapsed &&
|
||||||
e.msg.userID != userID()) setState(() => _hasNewMessages = true);
|
e.msg.userID != userID()) setState(() => _hasNewMessages = true);
|
||||||
});
|
});
|
||||||
@ -99,8 +99,7 @@ class _ConversationWindowState extends SafeState<ConversationWindow> {
|
|||||||
isCollapsed: _collapsed,
|
isCollapsed: _collapsed,
|
||||||
body: buildErrorCard(tr("Could not load conversation information!"),
|
body: buildErrorCard(tr("Could not load conversation information!"),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
ElevatedButton(
|
||||||
textColor: Colors.white,
|
|
||||||
onPressed: _refresh,
|
onPressed: _refresh,
|
||||||
child: Text(tr("Try again").toUpperCase()),
|
child: Text(tr("Try again").toUpperCase()),
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,7 @@ import 'package:comunic/models/membership.dart';
|
|||||||
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||||
import 'package:comunic/ui/routes/main_route/page_info.dart';
|
import 'package:comunic/ui/routes/main_route/page_info.dart';
|
||||||
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/conversation_image_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/group_icon_widget.dart';
|
import 'package:comunic/ui/widgets/group_icon_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/group_membership_widget.dart';
|
import 'package:comunic/ui/widgets/group_membership_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/safe_state.dart';
|
import 'package:comunic/ui/widgets/safe_state.dart';
|
||||||
@ -202,9 +203,21 @@ class _MembershipsPanelState extends SafeState<MembershipsPanel> {
|
|||||||
color: color,
|
color: color,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: Icon(Icons.message),
|
leading: ConversationImageWidget(
|
||||||
title: Text(
|
conversation: conversation,
|
||||||
ConversationsHelper.getConversationName(conversation, _usersList)),
|
users: _usersList,
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.message,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text(ConversationsHelper.getConversationName(
|
||||||
|
conversation, _usersList)),
|
||||||
|
],
|
||||||
|
),
|
||||||
subtitle: Text(diffTimeFromNowToStr(membership.lastActive) +
|
subtitle: Text(diffTimeFromNowToStr(membership.lastActive) +
|
||||||
(conversation.isHavingCall ? "\n" + tr("Ongoing call") : "")),
|
(conversation.isHavingCall ? "\n" + tr("Ongoing call") : "")),
|
||||||
onTap: () => MainController.of(context)
|
onTap: () => MainController.of(context)
|
||||||
|
@ -123,7 +123,7 @@ class _UserPageTabletState extends State<UserPageTablet> {
|
|||||||
// Friends list of the user
|
// Friends list of the user
|
||||||
_userInfo.isFriendsListPublic
|
_userInfo.isFriendsListPublic
|
||||||
? Expanded(
|
? Expanded(
|
||||||
child: OutlineButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () => MainController.of(context)
|
onPressed: () => MainController.of(context)
|
||||||
.openUserFriendsList(_userInfo.id),
|
.openUserFriendsList(_userInfo.id),
|
||||||
icon: Icon(Icons.group),
|
icon: Icon(Icons.group),
|
||||||
@ -135,7 +135,7 @@ class _UserPageTabletState extends State<UserPageTablet> {
|
|||||||
// Private messages
|
// Private messages
|
||||||
!_isCurrentUser
|
!_isCurrentUser
|
||||||
? Expanded(
|
? Expanded(
|
||||||
child: OutlineButton(
|
child: OutlinedButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
openPrivateConversation(context, _userInfo.id),
|
openPrivateConversation(context, _userInfo.id),
|
||||||
child: Icon(Icons.message),
|
child: Icon(Icons.message),
|
||||||
|
@ -27,7 +27,7 @@ class TextRichContentWidget extends StatelessWidget {
|
|||||||
static List<TextSpan> _parse(String text, TextStyle style) {
|
static List<TextSpan> _parse(String text, TextStyle style) {
|
||||||
if (style == null) style = TextStyle();
|
if (style == null) style = TextStyle();
|
||||||
|
|
||||||
List<TextSpan> list = List();
|
List<TextSpan> list = [];
|
||||||
String currString = "";
|
String currString = "";
|
||||||
|
|
||||||
text.split("\n").forEach((f) {
|
text.split("\n").forEach((f) {
|
||||||
|
@ -55,7 +55,7 @@ class TextWidget extends StatelessWidget {
|
|||||||
List<InlineSpan> _parseLinks(
|
List<InlineSpan> _parseLinks(
|
||||||
BuildContext context, String text, TextStyle style) {
|
BuildContext context, String text, TextStyle style) {
|
||||||
var buff = StringBuffer();
|
var buff = StringBuffer();
|
||||||
final list = new List<InlineSpan>();
|
final list = <InlineSpan>[];
|
||||||
|
|
||||||
// Change word function
|
// Change word function
|
||||||
final changeWordType = () {
|
final changeWordType = () {
|
||||||
|
84
lib/ui/widgets/user_writing_in_conv_notifier.dart
Normal file
84
lib/ui/widgets/user_writing_in_conv_notifier.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:comunic/helpers/events_helper.dart';
|
||||||
|
import 'package:comunic/helpers/server_config_helper.dart';
|
||||||
|
import 'package:comunic/helpers/users_helper.dart';
|
||||||
|
import 'package:comunic/lists/users_list.dart';
|
||||||
|
import 'package:comunic/ui/widgets/safe_state.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:comunic/utils/log_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// User writing a message in a conversation notifier
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class UserWritingInConvNotifier extends StatefulWidget {
|
||||||
|
final int convID;
|
||||||
|
|
||||||
|
const UserWritingInConvNotifier({Key key, @required this.convID})
|
||||||
|
: assert(convID != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_UserWritingInConvNotifierState createState() =>
|
||||||
|
_UserWritingInConvNotifierState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserWritingInConvNotifierState
|
||||||
|
extends SafeState<UserWritingInConvNotifier> {
|
||||||
|
final _usersInfo = UsersList();
|
||||||
|
|
||||||
|
final _list = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
this.listen<WritingMessageInConversationEvent>((ev) async {
|
||||||
|
try {
|
||||||
|
if (ev.convID != widget.convID) return;
|
||||||
|
|
||||||
|
if (!_usersInfo.hasUser(ev.userID))
|
||||||
|
_usersInfo.add(await UsersHelper().getSingleWithThrow(ev.userID));
|
||||||
|
|
||||||
|
_handleEvent(ev.userID);
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Padding(
|
||||||
|
padding: EdgeInsets.only(left: 5, right: 5),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
writingText,
|
||||||
|
style: TextStyle(fontSize: 10),
|
||||||
|
textAlign: TextAlign.justify,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
String get writingText {
|
||||||
|
if (_list.isEmpty) return "";
|
||||||
|
|
||||||
|
final users = _list.toSet().map((e) => _usersInfo.getUser(e)).toList();
|
||||||
|
|
||||||
|
if (users.length == 1)
|
||||||
|
return tr("%1% is writing...", args: {"1": users.first.fullName});
|
||||||
|
|
||||||
|
return tr("%1% and %2% are writing...", args: {
|
||||||
|
"1": users.first.fullName,
|
||||||
|
"2": users.sublist(1).map((e) => e.fullName).join(", ")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEvent(int userID) async {
|
||||||
|
setState(() => this._list.add(userID));
|
||||||
|
|
||||||
|
await Future.delayed(
|
||||||
|
Duration(seconds: srvConfig.conversationsPolicy.writingEventLifetime));
|
||||||
|
|
||||||
|
setState(() => this._list.removeAt(0));
|
||||||
|
}
|
||||||
|
}
|
@ -226,7 +226,7 @@ class _Element {
|
|||||||
/// Note : if text is not null, children must be empty !!!
|
/// Note : if text is not null, children must be empty !!!
|
||||||
String text;
|
String text;
|
||||||
final _ElementStyle style;
|
final _ElementStyle style;
|
||||||
final List<_Element> children = List();
|
final List<_Element> children = [];
|
||||||
|
|
||||||
_Element({@required this.style, this.text});
|
_Element({@required this.style, this.text});
|
||||||
|
|
||||||
|
14
lib/utils/clipboard_utils.dart
Normal file
14
lib/utils/clipboard_utils.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
import 'intl_utils.dart';
|
||||||
|
|
||||||
|
/// Clipboard utilities
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
void copyToClipboard(BuildContext context, String content) {
|
||||||
|
FlutterClipboard.copy(content);
|
||||||
|
snack(context, tr("'%1%' copied to clipboard!", args: {"1": content}));
|
||||||
|
}
|
14
lib/utils/color_utils.dart
Normal file
14
lib/utils/color_utils.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
/// Color utilities
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
String colorToHex(Color color) {
|
||||||
|
if (color == null) return "";
|
||||||
|
|
||||||
|
return (color.red.toRadixString(16).padLeft(2, '0') +
|
||||||
|
color.green.toRadixString(16).padLeft(2, '0') +
|
||||||
|
color.blue.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
@ -10,16 +10,16 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
/// Open a private conversation with a given [userID]
|
/// Open a private conversation with a given [userID]
|
||||||
Future<bool> openPrivateConversation(BuildContext context, int userID) async {
|
Future<bool> openPrivateConversation(BuildContext context, int userID) async {
|
||||||
|
try {
|
||||||
final convID = await ConversationsHelper().getPrivate(userID);
|
final convID = await ConversationsHelper().getPrivate(userID);
|
||||||
|
|
||||||
if (convID == null) {
|
|
||||||
showSimpleSnack(context, tr("Could not find a private conversation!"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the conversation
|
// Open the conversation
|
||||||
MainController.of(context).openConversation(convID);
|
MainController.of(context).openConversation(convID);
|
||||||
|
|
||||||
// Success
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch (e, s) {
|
||||||
|
print("Failed to find private conversation! $e => $s");
|
||||||
|
showSimpleSnack(context, tr("Could not find a private conversation!"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
14
lib/utils/dart_color.dart
Normal file
14
lib/utils/dart_color.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// https://stackoverflow.com/a/53905427
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class HexColor extends Color {
|
||||||
|
static int _getColorFromHex(String hexColor) {
|
||||||
|
hexColor = hexColor.toUpperCase().replaceAll("#", "");
|
||||||
|
if (hexColor.length == 6) {
|
||||||
|
hexColor = "FF" + hexColor;
|
||||||
|
}
|
||||||
|
return int.parse(hexColor, radix: 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user