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:
|
||||
flutter build apk --flavor beta -t lib/main_online.dart
|
||||
|
||||
beta_dev_release:
|
||||
flutter build apk --flavor beta -t lib/main_dev.dart
|
||||
|
||||
|
||||
beta_online_release_split_per_abi:
|
||||
flutter build apk --flavor beta -t lib/main_online.dart --target-platform android-arm,android-arm64,android-x64 --split-per-abi
|
||||
|
@ -3,7 +3,9 @@
|
||||
package="org.communiquons.comunic">
|
||||
|
||||
<application
|
||||
tools:replace="android:label"
|
||||
android:label="Comunic Beta" />
|
||||
android:label="Comunic Beta"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:label" />
|
||||
|
||||
|
||||
</manifest>
|
@ -4,4 +4,7 @@
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- Use clear connection in dev mode -->
|
||||
<application android:usesCleartextTraffic="true" />
|
||||
</manifest>
|
||||
|
@ -14,6 +14,16 @@
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- This is required on Android 11+ for image picker -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||
</intent>
|
||||
|
||||
<intent>
|
||||
<action android:name="android.media.action.VIDEO_CAPTURE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
@ -26,6 +36,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
@ -58,5 +70,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- This let the image cropper work -->
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
@ -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%d": "%days% j",
|
||||
"%hours% h": "%hours% h",
|
||||
@ -9,6 +16,7 @@
|
||||
"%num% members": "%num% membres",
|
||||
"%secs%s": "%secs% s",
|
||||
"%years% years": "%years% ans",
|
||||
"'%1%' copied to clipboard!": "'%1%' copié dans le presse papier !",
|
||||
"1 Like": "1 personne aime",
|
||||
"1 member": "1 membre",
|
||||
"1 month": "1 mois",
|
||||
@ -38,6 +46,7 @@
|
||||
"Add image": "Ajouter une image",
|
||||
"Add member": "Ajouter un membre",
|
||||
"Add new emoji": "Ajouter un nouvel émoticon",
|
||||
"Admin": "Admin",
|
||||
"Administrator": "Administrateur",
|
||||
"All members": "Tous les membres",
|
||||
"All the members of the group can create posts on the group": "Tous les membres du groupe peuvent créer des posts",
|
||||
@ -59,6 +68,8 @@
|
||||
"Appearance": "Apparence",
|
||||
"Application settings": "Paramètres de l'application",
|
||||
"Are you sure do you want to remove this friend from your list of friends ? A friendship request will have to be sent to get this user back to your list!": "Voulez-vous vraiment supprimer cet ami de votre liste d'amis ? Il faudra une demande d'ami pour réintégrer cet utilisateur à votre liste !",
|
||||
"Audio Player": "Lecteur audio",
|
||||
"Audio record": "Enregistrement audio",
|
||||
"Automatically delete unread notifications after": "Supprimer automatiquement les notifications non lues après",
|
||||
"Automatically delete your account if you have been inactive for": "Supprimer votre compte si vous avez été déconnecté pendant",
|
||||
"Automatically delete your comments after": "Supprimer automatiquement vos commentaires après",
|
||||
@ -66,6 +77,7 @@
|
||||
"Automatically delete your likes after": "Supprimer automatiquement vos \"J'aime\" après",
|
||||
"Automatically delete your posts after": "Supprimer automatiquement vos posts après",
|
||||
"Block the creation of new responses": "Bloquer la création de nouvelles réponses",
|
||||
"Browse files": "Parcourir les fichiers",
|
||||
"Camera": "Caméra",
|
||||
"Can access to all group posts": "Peut accéder à tous les posts du groupe",
|
||||
"Can always create posts, invite users and respond to membership request": "Peut toujours envoyer des posts, inviter des utilisateurs et répondre aux demande d'inscription au groupe",
|
||||
@ -75,14 +87,17 @@
|
||||
"Cancel response to survey": "Annuler la réponse au sondage",
|
||||
"Change account image visibility": "Changer la visibilité de l'image de compte",
|
||||
"Change level": "Changer le niveau",
|
||||
"Change logo": "Changer",
|
||||
"Change password": "Changer le mot de passe",
|
||||
"Change your password": "Changer de mot de passe",
|
||||
"Change your security questions": "Changer les questions de sécurité",
|
||||
"Checking availability...": "Vérification de la disponibilité...",
|
||||
"Choose a new password": "Choisir un nouveau mot de passe",
|
||||
"Choose a user": "Choisir un utilisateur",
|
||||
"Choose a video": "Choisir une vidéo",
|
||||
"Choose a virtual directory": "Choisir un répertoire virtuel",
|
||||
"Choose an image": "Choisir une image",
|
||||
"Close": "Fermer",
|
||||
"Closed registration": "Inscription fermée",
|
||||
"Comunic": "Comunic",
|
||||
"Comunic is a free and OpenSource social network that respect your privacy.": "Comunic est un réseau social libre qui respecte votre vie privée.",
|
||||
@ -95,9 +110,14 @@
|
||||
"Confirm your password": "Confirmer le mot de passe",
|
||||
"Congratulations! Your password has now been successfully changed!": "Félicitations ! Votre mot de passe a bien été changé !",
|
||||
"Connected users": "Utilisateurs connectés",
|
||||
"Conversation color (optional)": "Couleur de conversation (optionnel)",
|
||||
"Conversation logo": "Logo de la conversation",
|
||||
"Conversation members": "Membres de la conversation",
|
||||
"Conversation name (optional)": "Nom de la conversation (optionnel)",
|
||||
"Conversation name (optionnal)": "Nom de la conversation (optionnel)",
|
||||
"Conversations": "Conversations",
|
||||
"Copy URL": "Copier l'URL",
|
||||
"Copy message": "Copier le message",
|
||||
"Could not block the creation of new choices!": "Erreur lors du bloquage de la création de nouveaux choix !",
|
||||
"Could not cancel invitation!": "Erreur lors de l'annulation de l'invitation !",
|
||||
"Could not cancel your membership request!": "Erreur lors de l'annulation de votre demande à rejoindre ce groupe !",
|
||||
@ -137,6 +157,7 @@
|
||||
"Could not get user information!": "Impossible de récupérer les informations de l'utilisateur !",
|
||||
"Could not initialize call!": "Erreur lors de l'initialisation de l'appel !",
|
||||
"Could not invite a user!": "Erreur lors de l'envoi de l'invitation pour l'utilisateur !",
|
||||
"Could not leave the conversation!": "Erreur lors du retrait de la conversation !",
|
||||
"Could not load conversation information!": "Erreur lors de la récupération des informations sur la conversation !",
|
||||
"Could not load friendship information!": "Erreur lors de la récupération des informations sur cet ami !",
|
||||
"Could not load general settings!": "Erreur lors du chargement des paramètres généraux !",
|
||||
@ -190,6 +211,9 @@
|
||||
"Create a new post...": "Créer un nouveau post...",
|
||||
"Create an account": "Créer un compte",
|
||||
"Create the conversation": "Créer la conversation",
|
||||
"Created on": "Créé le",
|
||||
"Creator": "Créateur",
|
||||
"Crop Photo": "Rogner la photo",
|
||||
"Current account image": "Image de compte actuelle",
|
||||
"Current choices:": "Choix actuels :",
|
||||
"Current level: %level%": "Niveau actuel : %level%",
|
||||
@ -208,6 +232,7 @@
|
||||
"Delete logo": "Supprimer le logo",
|
||||
"Delete your account": "Supprimer votre compte",
|
||||
"Deprecated application version": "Version obsolète de l'application",
|
||||
"Did not get permission to access microphone!": "Permission d'accéder au microphone refusée !",
|
||||
"Disconnect all your devices": "Déconnecter tous vos appareils",
|
||||
"Disconnect all your devices from Comunic, including the current one. Use this option if one of the device you use for Comunic was stolen.": "Déconnecte tous vos appareils de Comunic, en incluant l'appareil actuel. Nous vous recommandons d'utiliser cette option si vous avez des raisons de penser que l'un des appareils que vous utiliser pour accéder à Comunic a été volé.",
|
||||
"Do you really want to block new choices creation?": "Voulez-vous vraiment bloquer la création de nouveaux choix ?",
|
||||
@ -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 group ? All the posts related to it will be permanently deleted!": "Voulez-vous vraiment supprimer ce groupe ? Tous les posts s'y rapportant seront également supprimés !",
|
||||
"Do you really want to delete this group membership ?": "Voulez-vous vraiment quitter ce groupe ?",
|
||||
"Do you really want to delete this logo?": "Voulez-vous vraiment supprimer ce logo ?",
|
||||
"Do you really want to delete this message ? The operation can not be cancelled !": "Voulez-vous vraiment supprimer ce message ? Cette opération est irréversible !",
|
||||
"Do you really want to delete this post ? The operation can not be reverted !": "Voulez-vous vraiment supprimer ce post ? Cette opération est irréversible !",
|
||||
"Do you really want to delete your account image ?": "Voulez-vous vraiment supprimer votre image de compte ?",
|
||||
"Do you really want to delete your account? This operation CAN NOT be reverted!": "Voulez-vous vraiment supprimer votre compte ? Cette opération NE PEUT PAS être annulée !",
|
||||
"Do you really want to disconnect all your devices from Comunic ?": "Voulez-vous vraiment déconnecter tous vos appareils de Comunic ?",
|
||||
"Do you really want to leave this call ?": "Voulez-vous vraiment quitter cet appel ?",
|
||||
"Do you really want to leave this conversation ?": "Voulez-vous vraiment quitter cette conversation ?",
|
||||
"Do you really want to leave this conversation ? As you are its last admin, it will be completely deleted!": "Voulez-vous vraiment quitter cette conversation ? Comme vous êtes son dernier administrateur, celle-ci sera complètement supprimée !",
|
||||
"Do you really want to reject this friendship request?": "Voulez-vous vraiment rejeter cette demande d'amis ?",
|
||||
"Do you really want to reject this invitation?": "Voulez-vous vraiment refuser cette invitation ?",
|
||||
"Do you really want to remove this conversation from your list of conversations ? If you are the owner of this conversation, it will be completely deleted!": "Voulez-vous vraiment supprimer la conversation de votre liste ? Si vous êtes le créateur de cette conversation, elle sera définitivement supprimée !",
|
||||
"Do you really want to remove this membership ?": "Voulez-vous vraiment supprimer cette inscription ?",
|
||||
"Do you really want to sign out from the application ?": "Voulez-vous vraiment vous déconnecter de l'application ?",
|
||||
"Do you want to unselected currently selected image ?": "Voulez-vous désélectionner l'image ?",
|
||||
"Done": "Terminé",
|
||||
"Download update outside Play Store": "Télécharger la mise hors du Play Store",
|
||||
"Email address": "Adresse e-mail",
|
||||
"Email address...": "Adresse mail...",
|
||||
"Enable dark theme": "Activer le thème sombre",
|
||||
"Error": "Erreur",
|
||||
"Error while creating your account": "Une erreur s'est produite lors de la création de votre compte.",
|
||||
"Error while pausing playback!": "Erreur lors de la pause de la lecture !",
|
||||
"Error while playing record!": "Erreur lors de la lecture de l'enregistrement !",
|
||||
"Error while processing new signal!": "Erreur lors du traitement d'un signal !",
|
||||
"Error while recording!": "Erreur lors de l'enregistrement !",
|
||||
"Error while resuming playback!": "Erreur lors de la reprise de la lecture !",
|
||||
"Error while stopping playback!": "Erreur lors de l'arrêt de la lecture !",
|
||||
"Everyone": "Tout le monde",
|
||||
"Everyone can choose to join the group without moderator approval": "Tout le monde peut rejoindre le groupe, sans l'approbation d'un modérateur",
|
||||
"Everyone can request a membership, but a moderator review the request": "Tout le monde peut demander à rejoindre le groupe, mais un modérateur doit accepter les demandes",
|
||||
"Failed to add member to conversation!": "Echec de l'ajout d'un membre à la conversation !",
|
||||
"Failed to change conversation logo !": "Erreur lors du changement de logo pour la conversation !",
|
||||
"Failed to choose an image!": "Erreur lors du choix d'une image !",
|
||||
"Failed to execute image cropper!": "Echec de l'exécution du rogneur d'image !",
|
||||
"Failed to initialize audio player!": "Echec de l'initialisation du lecteur audio !",
|
||||
"Failed to initialize video!": "Erreur lors de l'initialisation de la vidéo !",
|
||||
"Failed to load conversation settings!": "Echec du chargement des paramètres de la conversation !",
|
||||
"Failed to load message information!": "Echec du chargement des informations du message !",
|
||||
"Failed to load privacy settings!": "Erreur lors du chargement des paramètres de vie privée !",
|
||||
"Failed to pick an image for the post!": "Echec de la sélection d'une image pour le post !",
|
||||
"Failed to pick an image!": "Echec de la sélection d'une image !",
|
||||
"Failed to remove conversation logo!": "Erreur lors de la suppression du logo de la conversation !",
|
||||
"Failed to remove member!": "Echec de la suppression d'un membre !",
|
||||
"Failed to send a file!": "Erreur lors de l'envoi d'un fichier !",
|
||||
"Failed to start recording!": "Erreur lors du lancement de l'enregistrement !",
|
||||
"Failed to toggle admin status of user!": "Echec du changement du status administrateur d'un membre !",
|
||||
"Failed to update conversation settings!": "Echec de la mise à jour des paramètres de la conversation !",
|
||||
"Failed to update data conservation policy!": "Echec de la mise à jour des paramètres de vie privée !",
|
||||
"Failed to upload new account image!": "Echec de l'envoi de la nouvelle image de compte !",
|
||||
"First name": "Prénom",
|
||||
"Follow": "Suivre",
|
||||
"Follow conversation": "Suivre la conversation",
|
||||
@ -291,6 +342,7 @@
|
||||
"Invited": "Invité",
|
||||
"Last name": "Nom",
|
||||
"Learn more about us": "En savoir plus sur nous",
|
||||
"Leave": "Quitter",
|
||||
"Let us ask you one last time. Do you really want to delete your account? If you decide to do so, your data will be permanently removed from our servers, so we will not be able to recover your account. If you decide to proceed, the deletion process will start immediatly and you will automatically get disconnected from your account.": "Laissez-nous vous demander une dernière fois. Voulez-vous vraiment supprimer votre compte ? Si vous décidez de continuer, les données liées à votre compte vont être supprimées de manière permanente de nos serveurs, et nous ne seront pas en mesure de les restaurer. Si vous décidez de poursuivre, le processus de supprimer vas débuter immédiatement et vous serez automatiquement déconnecté de votre compte.",
|
||||
"Like": "J'aime",
|
||||
"Loading": "Chargement",
|
||||
@ -306,7 +358,10 @@
|
||||
"Members": "Membres",
|
||||
"Membership": "Inscription",
|
||||
"Menu": "Menu",
|
||||
"Message not seen yet": "Message non vu",
|
||||
"Message rejected by the server!": "Message rejeté par le serveur !",
|
||||
"Message seen": "Message vu",
|
||||
"Message statistics": "Statistiques du message",
|
||||
"Moderated registration": "Inscription modérée",
|
||||
"Moderator": "Modérateur",
|
||||
"Moderators only": "Modérateurs uniquement",
|
||||
@ -321,8 +376,10 @@
|
||||
"New comment...": "Nouveau commentaire...",
|
||||
"New content...": "Nouveau contenu...",
|
||||
"New content:": "Nouveau contenu :",
|
||||
"New file": "Nouveau fichier",
|
||||
"New membership level": "Nouveau niveau d'appartenance au groupe",
|
||||
"New message": "Nouveau message",
|
||||
"New message...": "Nouveau message...",
|
||||
"New password": "Nouveau mot de passe",
|
||||
"New survey": "Nouveau sondage",
|
||||
"Newest": "Plus récent",
|
||||
@ -350,6 +407,8 @@
|
||||
"Password required": "Mot de passe requis",
|
||||
"Permanently delete your account and all data related to it.": "Supprimer de manière permanente votre compte et toute les données qui y sont rattachées",
|
||||
"Personal website URL (optional)": "Site web personnel (optionnel)",
|
||||
"Playback paused...": "Lecture en pause",
|
||||
"Playing...": "Lecture...",
|
||||
"Please answer now your security questions:": "Veuillez répondre à vos questions de sécurité :",
|
||||
"Please choose new account image visibility level:": "Veuillez choisir un nouveau niveau de visibilité pour votre image de compte :",
|
||||
"Please enter message content: ": "Veuillez entrer le contenu du message :",
|
||||
@ -372,6 +431,9 @@
|
||||
"Question": "Question",
|
||||
"Question 1": "Question 1",
|
||||
"Question 2": "Question 2",
|
||||
"Ready": "Prêt",
|
||||
"Record audio": "Faire un enregistrement audio",
|
||||
"Recording...": "Enregistrement...",
|
||||
"Reject": "Rejeter",
|
||||
"Reject request": "Rejeter la demande",
|
||||
"Remove": "Supprimer",
|
||||
@ -401,21 +463,26 @@
|
||||
"Sign out": "Déconnexion",
|
||||
"Specified email address was not found!": "L'adresse mail spécifiée n'a pas été trouvée !",
|
||||
"Specify URL": "Spécifier l'URL",
|
||||
"Statistics": "Statistiques",
|
||||
"Stop streaming": "Arrêter de partager ma vidéo & mon audio",
|
||||
"Submit": "Valider",
|
||||
"Switch camera": "Changer de caméra",
|
||||
"Take a picture": "Prendre une photo",
|
||||
"Take a video": "Prendre une vidéo",
|
||||
"The group is accessible to accepted members only.": "Le groupe n'est accessible qu'à ses membres",
|
||||
"The group is visible only to invited members.": "Le groupe n'apparaît qu'à ses membres, et aux personnes invitées à le rejoindre.",
|
||||
"The only way to join the group is to be invited by a moderator": "Seul un modérateur peut inviter quelqu'un à rejoindre le groupe",
|
||||
"The password and its confirmation do not match!": "La confirmation ne correspond pas au mot de passe !",
|
||||
"The post has been successfully created!": "Le post a été créé avec succès !",
|
||||
"There is no message yet in this 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.",
|
||||
"This account is private.": "Ce compte est privé.",
|
||||
"This file could not be sent: it is too big! (Max allowed size: %1%)": "Ce fichier ne peut pas être envoyé : il est trop lourd ! (Taille maximale autorisée: %1%)",
|
||||
"This kind of notification is not supported yet by this application.": "Ce type de notification n'est pas encore supportée par l'application.",
|
||||
"This password is not the same as the other one!": "Ce mot de passe est différent de l'autre",
|
||||
"This version of the Comunic application is deprecated. You might still be able to use it, but some features may not work. We recommend you to update to the latest version of the application.": "Cette version de l'application Comunic est obsolète. Vous pouvez continuer à l'utiliser, mais certaines fonctionalités pourront ne plus fonctionner. Nous vous recommandons d'installer la dernière version de l'applicatioon.",
|
||||
"This virtual directory is invalid / unvailable !": "Ce répertoire virtuel est invalide / indisponible !",
|
||||
"Toggle admin status": "Changer le status d'admin",
|
||||
"Too many accounts have been created from this IP address for now. Please try again later.": "Trop de comptes ont été créés avec cette addresse IP pour l'instant. Veuillez ré-essayer plus tard.",
|
||||
"Too many unsuccessfull login attempts! Please try again later...": "Trop de tentatives de connexion ont échoué. Veuillez ré-essayer plus tard...",
|
||||
"Try again": "Essayer à nouveau",
|
||||
@ -424,6 +491,7 @@
|
||||
"Update a conversation": "Modifier une conversation",
|
||||
"Update comment content": "Modifier le contenu du commentaire",
|
||||
"Update content": "Modifier le contenu",
|
||||
"Update conversation": "Mise à jour d'une conversation",
|
||||
"Update message": "Modifier un message",
|
||||
"Update post content": "Modifier le contenu du post",
|
||||
"Update security questions": "Mise à jour des questions de sécurité",
|
||||
@ -447,7 +515,7 @@
|
||||
"You security questions have been successfully updated!": "Vos questions de sécurité ont été mises avec succès !",
|
||||
"You will need to restart the application to apply changes": "Vous aurez besoin de redémarrer l'application pour appliquer les changements",
|
||||
"YouTube movie": "Vidéo YouTube",
|
||||
"Your account has been successfully created. You can now login to start to use it.": "Votre compte a été créé avec succès. Vous pouvez à présent vous connecter 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 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.",
|
||||
|
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,
|
||||
)));
|
||||
}
|
||||
|
||||
// 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
|
||||
final response = await Dio().post(
|
||||
url.toString(),
|
||||
data: data,
|
||||
cancelToken: request.cancelToken,
|
||||
onSendProgress: request.progressCallback,
|
||||
options: Options(
|
||||
receiveDataWhenStatusError: true,
|
||||
validateStatus: (s) => true,
|
||||
|
@ -17,7 +17,7 @@ class CommentsHelper {
|
||||
"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();
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:comunic/helpers/database/conversation_messages_database_helper.dart';
|
||||
import 'package:comunic/helpers/database/conversations_database_helper.dart';
|
||||
import 'package:comunic/helpers/serialization/conversation_message_serialization_helper.dart';
|
||||
import 'package:comunic/helpers/serialization/conversations_serialization_helper.dart';
|
||||
import 'package:comunic/helpers/users_helper.dart';
|
||||
import 'package:comunic/helpers/websocket_helper.dart';
|
||||
import 'package:comunic/lists/conversation_messages_list.dart';
|
||||
@ -9,11 +9,17 @@ import 'package:comunic/lists/users_list.dart';
|
||||
import 'package:comunic/models/api_request.dart';
|
||||
import 'package:comunic/models/api_response.dart';
|
||||
import 'package:comunic/models/conversation.dart';
|
||||
import 'package:comunic/models/conversation_member.dart';
|
||||
import 'package:comunic/models/conversation_message.dart';
|
||||
import 'package:comunic/models/displayed_content.dart';
|
||||
import 'package:comunic/models/new_conversation.dart';
|
||||
import 'package:comunic/models/new_conversation_message.dart';
|
||||
import 'package:comunic/models/new_conversation_settings.dart';
|
||||
import 'package:comunic/models/unread_conversation.dart';
|
||||
import 'package:comunic/utils/account_utils.dart';
|
||||
import 'package:comunic/utils/color_utils.dart';
|
||||
import 'package:comunic/utils/dart_color.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Conversation helper
|
||||
@ -25,140 +31,147 @@ enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED }
|
||||
class ConversationsHelper {
|
||||
static final _registeredConversations = Map<int, int>();
|
||||
|
||||
final ConversationsDatabaseHelper _conversationsDatabaseHelper =
|
||||
ConversationsDatabaseHelper();
|
||||
final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper =
|
||||
ConversationMessagesDatabaseHelper();
|
||||
|
||||
/// Create a new conversation
|
||||
///
|
||||
/// Return the ID of the newly created conversation or -1 in case of failure
|
||||
Future<int> createConversation(Conversation settings) async {
|
||||
final response =
|
||||
await APIRequest(uri: "conversations/create", needLogin: true, args: {
|
||||
"name": settings.hasName ? settings.name : "false",
|
||||
"follow": settings.following ? "true" : "false",
|
||||
/// Return the ID of the newly created conversation
|
||||
///
|
||||
/// Throws in case of failure
|
||||
static Future<int> createConversation(NewConversation settings) async {
|
||||
final response = await APIRequest.withLogin("conversations/create", args: {
|
||||
"name": settings.name ?? "",
|
||||
"follow": settings.follow ? "true" : "false",
|
||||
"users": settings.members.join(","),
|
||||
}).addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers).exec();
|
||||
|
||||
if (response.code != 200) return -1;
|
||||
"color": colorToHex(settings.color)
|
||||
})
|
||||
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
|
||||
.execWithThrow();
|
||||
|
||||
return response.getObject()["conversationID"];
|
||||
}
|
||||
|
||||
/// Add a member to a conversation.
|
||||
///
|
||||
/// Throws in case of failure
|
||||
static Future<void> addMember(int convID, int userID) async =>
|
||||
await APIRequest.withLogin("conversations/addMember")
|
||||
.addInt("convID", convID)
|
||||
.addInt("userID", userID)
|
||||
.execWithThrow();
|
||||
|
||||
/// Remove a member from a conversation.
|
||||
///
|
||||
/// Throws in case of failure
|
||||
static Future<void> removeMember(int convID, int userID) async =>
|
||||
await APIRequest.withLogin("conversations/removeMember")
|
||||
.addInt("convID", convID)
|
||||
.addInt("userID", userID)
|
||||
.execWithThrow();
|
||||
|
||||
/// Update admin status of a user in a conversation
|
||||
///
|
||||
/// Throws in case of failure
|
||||
static Future<void> setAdmin(int convID, int userID, bool admin) async =>
|
||||
await APIRequest.withLogin("conversations/setAdmin")
|
||||
.addInt("convID", convID)
|
||||
.addInt("userID", userID)
|
||||
.addBool("setAdmin", admin)
|
||||
.execWithThrow();
|
||||
|
||||
/// Update an existing conversation
|
||||
///
|
||||
/// Returns a boolean depending of the success of the operation
|
||||
Future<bool> updateConversation(Conversation settings) async {
|
||||
final request =
|
||||
APIRequest(uri: "conversations/updateSettings", needLogin: true, args: {
|
||||
"conversationID": settings.id.toString(),
|
||||
"following": settings.following ? "true" : "false"
|
||||
});
|
||||
/// Throws in case of failure
|
||||
static Future<void> updateConversation(
|
||||
NewConversationsSettings settings) async {
|
||||
final request = APIRequest.withLogin("conversations/updateSettings")
|
||||
.addInt("conversationID", settings.convID)
|
||||
.addBool("following", settings.following);
|
||||
|
||||
if (settings.isOwner || settings.canEveryoneAddMembers)
|
||||
request.addString("members", settings.members.join(","));
|
||||
// Update conversation settings
|
||||
if (settings.isComplete)
|
||||
request
|
||||
.addString("name", settings.name ?? "")
|
||||
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
|
||||
.addString("color", colorToHex(settings.color));
|
||||
|
||||
// Update all conversation settings, if possible
|
||||
if (settings.isOwner) {
|
||||
request.addString("name", settings.hasName ? settings.name : "false");
|
||||
request.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers);
|
||||
await request.execWithThrow();
|
||||
|
||||
// Delete old conversation entry from the database
|
||||
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;
|
||||
|
||||
//Delete old conversation entry from the database
|
||||
await _conversationsDatabaseHelper.delete(settings.id);
|
||||
|
||||
// Success
|
||||
return true;
|
||||
}
|
||||
/// Remove conversation logo
|
||||
///
|
||||
/// Throws in case of failure
|
||||
static Future<void> removeLogo(int convID) async =>
|
||||
await APIRequest.withLogin("conversations/delete_image")
|
||||
.addInt("convID", convID)
|
||||
.execWithThrow();
|
||||
|
||||
/// Delete a conversation specified by its [id]
|
||||
Future<bool> deleteConversation(int id) async {
|
||||
final response = await APIRequest(
|
||||
uri: "conversations/delete",
|
||||
needLogin: true,
|
||||
args: {
|
||||
"conversationID": id.toString(),
|
||||
},
|
||||
).exec();
|
||||
|
||||
return response.code == 200;
|
||||
}
|
||||
Future<void> deleteConversation(int id) async =>
|
||||
await APIRequest.withLogin("conversations/delete")
|
||||
.addInt("conversationID", id)
|
||||
.execWithThrow();
|
||||
|
||||
/// Download the list of conversations from the server
|
||||
///
|
||||
/// Throws an exception in case of failure
|
||||
Future<ConversationsList> downloadList() async {
|
||||
final response =
|
||||
await APIRequest(uri: "conversations/getList", needLogin: true).exec();
|
||||
await APIRequest.withLogin("conversations/getList").execWithThrow();
|
||||
|
||||
if (response.code != 200) return null;
|
||||
|
||||
try {
|
||||
ConversationsList list = ConversationsList();
|
||||
response.getArray().forEach((f) => list.add(apiToConversation(f)));
|
||||
|
||||
// Update the database
|
||||
await _conversationsDatabaseHelper.clearTable();
|
||||
await _conversationsDatabaseHelper.insertAll(list);
|
||||
await ConversationsSerializationHelper().setList(list);
|
||||
|
||||
return list;
|
||||
} on Exception catch (e) {
|
||||
print(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the local list of conversations
|
||||
Future<ConversationsList> getCachedList() async {
|
||||
final list = await _conversationsDatabaseHelper.getAll();
|
||||
final list = await ConversationsSerializationHelper().getList();
|
||||
list.sort();
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Get information about a single conversation specified by its [id]
|
||||
Future<Conversation> _downloadSingle(int id) async {
|
||||
try {
|
||||
final response = await APIRequest(
|
||||
uri: "conversations/getInfoOne",
|
||||
uri: "conversations/get_single",
|
||||
needLogin: true,
|
||||
args: {"conversationID": id.toString()}).exec();
|
||||
|
||||
if (response.code != 200) return null;
|
||||
args: {"conversationID": id.toString()}).execWithThrow();
|
||||
|
||||
final conversation = apiToConversation(response.getObject());
|
||||
_conversationsDatabaseHelper.insertOrUpdate(conversation);
|
||||
|
||||
await ConversationsSerializationHelper()
|
||||
.insertOrReplaceElement((c) => c.id == conversation.id, conversation);
|
||||
return conversation;
|
||||
} on Exception catch (e) {
|
||||
print(e.toString());
|
||||
print("Could not get information about a single conversation !");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get information about a single conversation. If [force] is set to false,
|
||||
/// Get information about a conversation. If [force] is set to false, a
|
||||
/// cached version of the conversation will be used, else it will always get
|
||||
/// the information from the server
|
||||
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
|
||||
/// the information from the server. The method throws an [Exception] in
|
||||
/// case of failure
|
||||
///
|
||||
/// Return value of this method is never null.
|
||||
Future<Conversation> getSingleOrThrow(int id, {bool force = false}) async {
|
||||
final conv = await this.getSingle(id, force: force);
|
||||
|
||||
if (conv == null)
|
||||
throw Exception("Could not get information about the conversation!");
|
||||
|
||||
return conv;
|
||||
Future<Conversation> getSingle(int id, {bool force = false}) async {
|
||||
if (force ||
|
||||
!await ConversationsSerializationHelper().any((c) => c.id == id))
|
||||
return await _downloadSingle(id);
|
||||
else
|
||||
return await ConversationsSerializationHelper().get(id);
|
||||
}
|
||||
|
||||
/// Get the name of a [conversation]. This requires information
|
||||
@ -170,9 +183,9 @@ class ConversationsHelper {
|
||||
String name = "";
|
||||
int count = 0;
|
||||
for (int i = 0; i < 3 && i < conversation.members.length; i++)
|
||||
if (conversation.members[i] != userID()) {
|
||||
if (conversation.members[i].userID != userID()) {
|
||||
name += (count > 0 ? ", " : "") +
|
||||
users.getUser(conversation.members[i]).fullName;
|
||||
users.getUser(conversation.members[i].userID).fullName;
|
||||
count++;
|
||||
}
|
||||
|
||||
@ -184,6 +197,8 @@ class ConversationsHelper {
|
||||
/// Search and return a private conversation with a given [userID]. If such
|
||||
/// conversation does not exists, it is created if [allowCreate] is set to
|
||||
/// true
|
||||
///
|
||||
/// Throws an exception in case of failure
|
||||
Future<int> getPrivate(int userID, {bool allowCreate = true}) async {
|
||||
final response = await APIRequest(
|
||||
uri: "conversations/getPrivate",
|
||||
@ -192,17 +207,10 @@ class ConversationsHelper {
|
||||
"otherUser": userID.toString(),
|
||||
"allowCreate": allowCreate.toString()
|
||||
},
|
||||
).exec();
|
||||
|
||||
if (response.code != 200) return null;
|
||||
).execWithThrow();
|
||||
|
||||
// Get and return conversation ID
|
||||
try {
|
||||
return int.parse(response.getObject()["conversationsID"][0].toString());
|
||||
} catch (e) {
|
||||
e.toString();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Asynchronously get the name of the conversation
|
||||
@ -210,15 +218,13 @@ class ConversationsHelper {
|
||||
/// Unlike the synchronous method, this method does not need information
|
||||
/// about the members of the conversation
|
||||
///
|
||||
/// Returns null in case of failure
|
||||
/// Throws an exception in case of failure
|
||||
static Future<String> getConversationNameAsync(
|
||||
Conversation conversation) async {
|
||||
if (conversation.hasName) return conversation.name;
|
||||
|
||||
//Get information about the members of the conversation
|
||||
final members = await UsersHelper().getUsersInfo(conversation.members);
|
||||
|
||||
if (members == null) return null;
|
||||
final members = await UsersHelper().getList(conversation.membersID);
|
||||
|
||||
return ConversationsHelper.getConversationName(conversation, members);
|
||||
}
|
||||
@ -226,14 +232,18 @@ class ConversationsHelper {
|
||||
/// Turn an API entry into a [Conversation] object
|
||||
static Conversation apiToConversation(Map<String, dynamic> map) {
|
||||
return Conversation(
|
||||
id: map["ID"],
|
||||
ownerID: map["ID_owner"],
|
||||
lastActive: map["last_active"],
|
||||
name: map["name"] == false ? null : map["name"],
|
||||
following: map["following"] == 1,
|
||||
sawLastMessage: map["saw_last_message"] == 1,
|
||||
members: List<int>.from(map["members"]),
|
||||
canEveryoneAddMembers: map["canEveryoneAddMembers"],
|
||||
id: map["id"],
|
||||
lastActivity: map["last_activity"],
|
||||
name: map["name"],
|
||||
color: map["color"] == null ? null : HexColor(map["color"]),
|
||||
logoURL: map["logo"],
|
||||
groupID: map["group_id"],
|
||||
members: map["members"]
|
||||
.cast<Map<String, dynamic>>()
|
||||
.map(apiToConversationMember)
|
||||
.toList()
|
||||
.cast<ConversationMember>(),
|
||||
canEveryoneAddMembers: map["can_everyone_add_members"],
|
||||
callCapabilities: map["can_have_video_call"]
|
||||
? CallCapabilities.VIDEO
|
||||
: (map["can_have_call"]
|
||||
@ -242,10 +252,21 @@ class ConversationsHelper {
|
||||
isHavingCall: map["has_call_now"]);
|
||||
}
|
||||
|
||||
static ConversationMember apiToConversationMember(Map<String, dynamic> map) =>
|
||||
ConversationMember(
|
||||
userID: map["user_id"],
|
||||
lastMessageSeen: map["last_message_seen"],
|
||||
lastAccessTime: map["last_access"],
|
||||
following: map["following"],
|
||||
isAdmin: map["is_admin"],
|
||||
);
|
||||
|
||||
/// Parse a list of messages given by the server
|
||||
///
|
||||
/// Throws an exception in case of failure
|
||||
Future<ConversationMessagesList> _parseConversationMessageFromServer(
|
||||
int conversationID, APIResponse response) async {
|
||||
if (response.code != 200) return null;
|
||||
response.assertOk();
|
||||
|
||||
// Parse the response of the server
|
||||
ConversationMessagesList list = ConversationMessagesList();
|
||||
@ -256,7 +277,8 @@ class ConversationsHelper {
|
||||
});
|
||||
|
||||
// Save messages in the cache
|
||||
_conversationMessagesDatabaseHelper.insertOrUpdateAll(list);
|
||||
await ConversationsMessagesSerializationHelper(conversationID)
|
||||
.insertOrReplaceAll(list);
|
||||
|
||||
return list;
|
||||
}
|
||||
@ -265,6 +287,8 @@ class ConversationsHelper {
|
||||
///
|
||||
/// Set [lastMessageID] to 0 to specify that we do not have any message of the
|
||||
/// conversation yet or another value else
|
||||
///
|
||||
/// Throws an exception in case of failure
|
||||
Future<ConversationMessagesList> _downloadNewMessagesSingle(
|
||||
int conversationID,
|
||||
{int lastMessageID = 0}) async {
|
||||
@ -275,26 +299,26 @@ class ConversationsHelper {
|
||||
args: {
|
||||
"conversationID": conversationID.toString(),
|
||||
"last_message_id": lastMessageID.toString()
|
||||
}).exec();
|
||||
}).execWithThrow();
|
||||
|
||||
return await _parseConversationMessageFromServer(conversationID, response);
|
||||
}
|
||||
|
||||
/// Get older messages for a given conversation from an online source
|
||||
///
|
||||
/// Throws in case of failure
|
||||
Future<ConversationMessagesList> getOlderMessages({
|
||||
@required int conversationID,
|
||||
@required int oldestMessagesID,
|
||||
int limit = 15,
|
||||
}) async {
|
||||
// Perform the request online
|
||||
final response = await APIRequest(
|
||||
uri: "conversations/get_older_messages",
|
||||
needLogin: true,
|
||||
args: {
|
||||
final response =
|
||||
await APIRequest.withLogin("conversations/get_older_messages", args: {
|
||||
"conversationID": conversationID.toString(),
|
||||
"oldest_message_id": oldestMessagesID.toString(),
|
||||
"limit": limit.toString()
|
||||
}).exec();
|
||||
}).execWithThrow();
|
||||
|
||||
return await _parseConversationMessageFromServer(conversationID, response);
|
||||
}
|
||||
@ -304,6 +328,8 @@ class ConversationsHelper {
|
||||
/// If [lastMessageID] is set to 0 then we retrieve the last messages of
|
||||
/// the conversation.
|
||||
/// Otherwise [lastMessageID] contains the ID of the last known message
|
||||
///
|
||||
/// Throws in case of failure
|
||||
Future<ConversationMessagesList> getNewMessages(
|
||||
{@required int conversationID,
|
||||
int lastMessageID = 0,
|
||||
@ -312,35 +338,32 @@ class ConversationsHelper {
|
||||
return await _downloadNewMessagesSingle(conversationID,
|
||||
lastMessageID: lastMessageID);
|
||||
else
|
||||
return await _conversationMessagesDatabaseHelper
|
||||
.getAllMessagesConversations(conversationID,
|
||||
lastMessageID: lastMessageID);
|
||||
}
|
||||
|
||||
/// Get a single conversation message from the local database
|
||||
///
|
||||
/// Returns the message if found or null in case of failure
|
||||
Future<ConversationMessage> getSingleMessageFromCache(int messageID) async {
|
||||
return await _conversationMessagesDatabaseHelper.get(messageID);
|
||||
return await ConversationsMessagesSerializationHelper(conversationID)
|
||||
.getList();
|
||||
}
|
||||
|
||||
/// Send a new message to the server
|
||||
Future<SendMessageResult> sendMessage(NewConversationMessage message) async {
|
||||
final request = APIRequest(
|
||||
uri: "conversations/sendMessage",
|
||||
needLogin: true,
|
||||
args: {
|
||||
"conversationID": message.conversationID.toString(),
|
||||
"message": message.hasMessage ? message.message : ""
|
||||
},
|
||||
);
|
||||
Future<SendMessageResult> sendMessage(
|
||||
NewConversationMessage message, {
|
||||
ProgressCallback sendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) async {
|
||||
final request = APIRequest.withLogin("conversations/sendMessage")
|
||||
.addInt("conversationID", message.conversationID)
|
||||
.addString("message", message.hasMessage ? message.message : "");
|
||||
|
||||
// Check for image
|
||||
if (message.hasImage) request.addPickedFile("image", message.image);
|
||||
request.progressCallback = sendProgress;
|
||||
request.cancelToken = cancelToken;
|
||||
|
||||
// Check for file
|
||||
if (message.hasFile) request.addBytesFile("file", message.file);
|
||||
|
||||
if (message.hasThumbnail)
|
||||
request.addBytesFile("thumbnail", message.thumbnail);
|
||||
|
||||
//Send the message
|
||||
APIResponse response;
|
||||
if (!message.hasImage)
|
||||
if (!message.hasFile)
|
||||
response = await request.exec();
|
||||
else
|
||||
response = await request.execWithFiles();
|
||||
@ -353,14 +376,13 @@ class ConversationsHelper {
|
||||
}
|
||||
|
||||
/// Save / Update a message into the database
|
||||
Future<void> saveMessage(ConversationMessage msg) async {
|
||||
await _conversationMessagesDatabaseHelper.insertOrUpdate(msg);
|
||||
}
|
||||
Future<void> saveMessage(ConversationMessage msg) async =>
|
||||
await ConversationsMessagesSerializationHelper(msg.convID)
|
||||
.insertOrReplace(msg);
|
||||
|
||||
/// Remove a message from the database
|
||||
Future<void> removeMessage(int msgID) async {
|
||||
await _conversationMessagesDatabaseHelper.delete(msgID);
|
||||
}
|
||||
Future<void> removeMessage(ConversationMessage msg) async =>
|
||||
await ConversationsMessagesSerializationHelper(msg.convID).remove(msg);
|
||||
|
||||
/// Update a message content
|
||||
Future<bool> updateMessage(int id, String newContent) async {
|
||||
@ -397,11 +419,8 @@ class ConversationsHelper {
|
||||
|
||||
return UnreadConversationsList()
|
||||
..addAll(list.map((f) => UnreadConversation(
|
||||
id: f["id"],
|
||||
convName: f["conv_name"],
|
||||
lastActive: f["last_active"],
|
||||
userID: f["userID"],
|
||||
message: f["message"],
|
||||
conv: apiToConversation(f["conv"]),
|
||||
message: apiToConversationMessage(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
|
||||
static ConversationMessage apiToConversationMessage(
|
||||
Map<String, dynamic> map,
|
||||
) {
|
||||
return ConversationMessage(
|
||||
id: map["ID"],
|
||||
conversationID: map["convID"],
|
||||
userID: map["ID_user"],
|
||||
timeInsert: map["time_insert"],
|
||||
message: DisplayedString(map["message"]),
|
||||
imageURL: map["image_path"],
|
||||
var file;
|
||||
if (map["file"] != null) {
|
||||
final fileMap = map["file"];
|
||||
file = ConversationMessageFile(
|
||||
url: fileMap["url"],
|
||||
size: fileMap["size"],
|
||||
name: fileMap["name"],
|
||||
thumbnail: fileMap["thumbnail"],
|
||||
type: fileMap["type"],
|
||||
);
|
||||
}
|
||||
|
||||
var serverMessage;
|
||||
if (map["server_message"] != null) {
|
||||
final srvMessageMap = map["server_message"];
|
||||
|
||||
var messageType;
|
||||
switch (srvMessageMap["type"]) {
|
||||
case "user_created_conv":
|
||||
messageType = ConversationServerMessageType.USER_CREATED_CONVERSATION;
|
||||
break;
|
||||
|
||||
case "user_added_another":
|
||||
messageType = ConversationServerMessageType.USER_ADDED_ANOTHER_USER;
|
||||
break;
|
||||
|
||||
case "user_left":
|
||||
messageType = ConversationServerMessageType.USER_LEFT_CONV;
|
||||
break;
|
||||
|
||||
case "user_removed_another":
|
||||
messageType = ConversationServerMessageType.USER_REMOVED_ANOTHER_USER;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception(
|
||||
"${srvMessageMap["type"]} is an unknown server message type!");
|
||||
}
|
||||
|
||||
serverMessage = ConversationServerMessage(
|
||||
type: messageType,
|
||||
userID: srvMessageMap["user_id"],
|
||||
userWhoAdded: srvMessageMap["user_who_added"],
|
||||
userAdded: srvMessageMap["user_added"],
|
||||
userWhoRemoved: srvMessageMap["user_who_removed"],
|
||||
userRemoved: srvMessageMap["user_removed"],
|
||||
);
|
||||
}
|
||||
|
||||
return ConversationMessage(
|
||||
id: map["id"],
|
||||
convID: map["conv_id"],
|
||||
userID: map["user_id"],
|
||||
timeSent: map["time_sent"],
|
||||
message: DisplayedString(map["message"] ?? ""),
|
||||
file: file,
|
||||
serverMessage: serverMessage);
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
||||
/// 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
|
||||
abstract class FriendsListTableContract {
|
||||
static const TABLE_NAME = "friends";
|
||||
|
@ -45,14 +45,6 @@ abstract class DatabaseHelper {
|
||||
// Drop users table
|
||||
await db.execute("DROP TABLE IF EXISTS ${UserTableContract.TABLE_NAME}");
|
||||
|
||||
// Drop conversations table
|
||||
await db.execute(
|
||||
"DROP TABLE IF EXISTS ${ConversationTableContract.TABLE_NAME}");
|
||||
|
||||
// Drop conversations messages table
|
||||
await db.execute(
|
||||
"DROP TABLE IF EXISTS ${ConversationsMessagesTableContract.TABLE_NAME}");
|
||||
|
||||
// Drop friends list table
|
||||
await db
|
||||
.execute("DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}");
|
||||
@ -74,29 +66,6 @@ abstract class DatabaseHelper {
|
||||
"${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
|
||||
await db.execute("CREATE TABLE ${FriendsListTableContract.TABLE_NAME} ("
|
||||
"${FriendsListTableContract.C_ID} INTEGER PRIMARY KEY, "
|
||||
|
@ -50,6 +50,14 @@ class DeletedCommentEvent {
|
||||
DeletedCommentEvent(this.commentID);
|
||||
}
|
||||
|
||||
/// Writing message in conversation event
|
||||
class WritingMessageInConversationEvent {
|
||||
final int convID;
|
||||
final int userID;
|
||||
|
||||
WritingMessageInConversationEvent(this.convID, this.userID);
|
||||
}
|
||||
|
||||
/// New conversation message
|
||||
class NewConversationMessageEvent {
|
||||
final ConversationMessage msg;
|
||||
@ -71,6 +79,21 @@ class DeletedConversationMessageEvent {
|
||||
DeletedConversationMessageEvent(this.msg);
|
||||
}
|
||||
|
||||
/// Remove user from conversation
|
||||
class RemovedUserFromConversationEvent {
|
||||
final int convID;
|
||||
final int userID;
|
||||
|
||||
RemovedUserFromConversationEvent(this.convID, this.userID);
|
||||
}
|
||||
|
||||
/// Deleted conversation
|
||||
class DeletedConversationEvent {
|
||||
final int convID;
|
||||
|
||||
DeletedConversationEvent(this.convID);
|
||||
}
|
||||
|
||||
/// User joined call event
|
||||
class UserJoinedCallEvent {
|
||||
final int callID;
|
||||
|
@ -142,7 +142,7 @@ class PostsHelper {
|
||||
break;
|
||||
|
||||
case PostKind.IMAGE:
|
||||
request.addPickedFile("image", post.image);
|
||||
request.addBytesFile("image", post.image);
|
||||
break;
|
||||
|
||||
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 dataConservationPolicy = response["data_conservation_policy"];
|
||||
final conversationsPolicy = response["conversations_policy"];
|
||||
|
||||
_config = ServerConfig(
|
||||
minSupportedMobileVersion:
|
||||
@ -50,7 +51,22 @@ class ServerConfigurationHelper {
|
||||
dataConservationPolicy["min_conversation_messages_lifetime"],
|
||||
minLikesLifetime: dataConservationPolicy["min_likes_lifetime"],
|
||||
),
|
||||
);
|
||||
conversationsPolicy: ConversationsPolicy(
|
||||
minMessageLen: conversationsPolicy["min_message_len"],
|
||||
maxMessageLen: conversationsPolicy["max_message_len"],
|
||||
allowedFilesType:
|
||||
conversationsPolicy["allowed_files_type"].cast<String>(),
|
||||
filesMaxSize: conversationsPolicy["files_max_size"],
|
||||
writingEventInterval: conversationsPolicy["writing_event_interval"],
|
||||
writingEventLifetime: conversationsPolicy["writing_event_lifetime"],
|
||||
maxMessageImageWidth: conversationsPolicy["max_message_image_width"],
|
||||
maxMessageImageHeight:
|
||||
conversationsPolicy["max_message_image_height"],
|
||||
maxThumbnailWidth: conversationsPolicy["max_thumbnail_width"],
|
||||
maxThumbnailHeight: conversationsPolicy["max_thumbnail_height"],
|
||||
maxLogoWidth: conversationsPolicy["max_logo_width"],
|
||||
maxLogoHeight: conversationsPolicy["max_logo_height"],
|
||||
));
|
||||
}
|
||||
|
||||
/// Get current server configuration, throwing if it is not loaded yet
|
||||
@ -62,3 +78,6 @@ class ServerConfigurationHelper {
|
||||
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/new_emoji.dart';
|
||||
import 'package:comunic/models/security_settings.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../models/api_request.dart';
|
||||
|
||||
/// Settings helper
|
||||
///
|
||||
@ -92,11 +93,10 @@ class SettingsHelper {
|
||||
}
|
||||
|
||||
/// Upload a new account image
|
||||
static Future<bool> uploadAccountImage(PickedFile newImage) async =>
|
||||
(await APIRequest(uri: "settings/upload_account_image", needLogin: true)
|
||||
.addPickedFile("picture", newImage)
|
||||
.execWithFiles())
|
||||
.isOK;
|
||||
static Future<void> uploadAccountImage(BytesFile newImage) async =>
|
||||
await APIRequest(uri: "settings/upload_account_image", needLogin: true)
|
||||
.addBytesFile("picture", newImage)
|
||||
.execWithFilesAndThrow();
|
||||
|
||||
/// Upload a new account image from memory
|
||||
static Future<bool> uploadAccountImageFromMemory(List<int> bytes) async =>
|
||||
@ -128,13 +128,12 @@ class SettingsHelper {
|
||||
|
||||
/// Upload a new custom emoji
|
||||
static Future<void> uploadNewCustomEmoji(NewEmoji newEmoji) async =>
|
||||
(await APIRequest(
|
||||
await APIRequest(
|
||||
uri: "settings/upload_custom_emoji",
|
||||
needLogin: true,
|
||||
args: {"shortcut": newEmoji.shortcut})
|
||||
.addPickedFile("image", newEmoji.image)
|
||||
.execWithFiles())
|
||||
.assertOk();
|
||||
.addBytesFile("image", newEmoji.image)
|
||||
.execWithFilesAndThrow();
|
||||
|
||||
/// Delete a custom emoji
|
||||
///
|
||||
@ -220,7 +219,8 @@ class SettingsHelper {
|
||||
/// Throws in case of failure
|
||||
static Future<void> setDataConservationPolicy(
|
||||
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)
|
||||
.addInt("inactive_account_lifetime",
|
||||
newSettings.inactiveAccountLifeTime ?? 0)
|
||||
|
@ -91,9 +91,16 @@ class UsersHelper {
|
||||
}
|
||||
|
||||
/// Get users information from a given [Set]
|
||||
///
|
||||
/// Throws in case of failure
|
||||
Future<UsersList> getList(Set<int> users,
|
||||
{bool forceDownload = false}) async {
|
||||
return await getUsersInfo(users.toList());
|
||||
final list = await getUsersInfo(users.toList());
|
||||
|
||||
if (list == null)
|
||||
throw Exception("Failed to get the list of users!");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Get users information
|
||||
@ -102,7 +109,7 @@ class UsersHelper {
|
||||
/// the server, otherwise cached data will be used if available
|
||||
Future<UsersList> getUsersInfo(List<int> users,
|
||||
{bool forceDownload = false}) async {
|
||||
List<int> toDownload = List();
|
||||
List<int> toDownload = [];
|
||||
UsersList list = UsersList();
|
||||
|
||||
// Check cache
|
||||
|
@ -144,6 +144,12 @@ class WebSocketHelper {
|
||||
EventsHelper.emit(DeletedCommentEvent(msg.data));
|
||||
break;
|
||||
|
||||
// A user is writing a new message
|
||||
case "writing_message_in_conv":
|
||||
EventsHelper.emit(WritingMessageInConversationEvent(
|
||||
msg.data["conv_id"], msg.data["user_id"]));
|
||||
break;
|
||||
|
||||
// Created new conversation message
|
||||
case "new_conv_message":
|
||||
EventsHelper.emit(NewConversationMessageEvent(
|
||||
@ -162,6 +168,17 @@ class WebSocketHelper {
|
||||
ConversationsHelper.apiToConversationMessage(msg.data)));
|
||||
break;
|
||||
|
||||
// Removed user from conversation
|
||||
case "removed_user_from_conv":
|
||||
EventsHelper.emit(RemovedUserFromConversationEvent(
|
||||
msg.data["conv_id"], msg.data["user_id"]));
|
||||
break;
|
||||
|
||||
// Conversation deleted
|
||||
case "deleted_conversation":
|
||||
EventsHelper.emit(DeletedConversationEvent(msg.data));
|
||||
break;
|
||||
|
||||
// A user joined a call
|
||||
case "user_joined_call":
|
||||
EventsHelper.emit(
|
||||
|
@ -5,7 +5,7 @@ import 'dart:collection';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class AbstractList<E> extends ListBase<E> {
|
||||
final _list = List<E>();
|
||||
final _list = <E>[];
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
|
@ -9,7 +9,7 @@ import 'package:comunic/models/comment.dart';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class CommentsList extends ListBase<Comment> {
|
||||
List<Comment> _list = List();
|
||||
List<Comment> _list = [];
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
|
@ -7,7 +7,7 @@ import 'package:comunic/models/conversation_message.dart';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class ConversationMessagesList extends ListBase<ConversationMessage> {
|
||||
final List<ConversationMessage> _list = List();
|
||||
final List<ConversationMessage> _list = [];
|
||||
|
||||
set length(int v) => _list.length = v;
|
||||
|
||||
@ -24,11 +24,10 @@ class ConversationMessagesList extends ListBase<ConversationMessage> {
|
||||
}
|
||||
|
||||
/// Get the list of the users ID who own a message in this list
|
||||
List<int> getUsersID() {
|
||||
final List<int> users = List();
|
||||
Set<int> getUsersID() {
|
||||
final Set<int> users = Set();
|
||||
|
||||
for (ConversationMessage message in this)
|
||||
if (!users.contains(message.userID)) users.add(message.userID);
|
||||
for (ConversationMessage message in this) users.addAll(message.usersID);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
@ -8,11 +8,11 @@ import 'package:comunic/models/conversation.dart';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class ConversationsList extends ListBase<Conversation> {
|
||||
|
||||
final List<Conversation> _list = List();
|
||||
final List<Conversation> _list = [];
|
||||
UsersList users;
|
||||
|
||||
set length(l) => _list.length = l;
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
@override
|
||||
@ -22,12 +22,9 @@ class ConversationsList extends ListBase<Conversation> {
|
||||
void operator []=(int index, Conversation value) => _list[index] = value;
|
||||
|
||||
/// Get the entire lists of users ID in this list
|
||||
List<int> get allUsersID {
|
||||
final List<int> list = List();
|
||||
forEach((c) => c.members.forEach((id){
|
||||
if(!list.contains(id))
|
||||
list.add(id);
|
||||
}));
|
||||
Set<int> get allUsersID {
|
||||
final Set<int> list = Set();
|
||||
forEach((c) => c.members.forEach((member) => list.add(member.userID)));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import 'package:comunic/models/friend.dart';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class FriendsList extends ListBase<Friend> {
|
||||
List<Friend> _list = List();
|
||||
List<Friend> _list = [];
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
|
@ -18,7 +18,7 @@ class MembershipList extends AbstractList<Membership> {
|
||||
case MembershipType.GROUP:
|
||||
break;
|
||||
case MembershipType.CONVERSATION:
|
||||
s.addAll(m.conversation.members);
|
||||
s.addAll(m.conversation.membersID);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -9,7 +9,7 @@ import 'package:comunic/models/post.dart';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class PostsList extends ListBase<Post> {
|
||||
List<Post> _list = List();
|
||||
List<Post> _list = [];
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
|
@ -7,5 +7,9 @@ import 'package:comunic/models/unread_conversation.dart';
|
||||
|
||||
class UnreadConversationsList extends AbstractList<UnreadConversation> {
|
||||
/// 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
|
||||
|
||||
class UsersList extends ListBase<User> {
|
||||
List<User> _list = List();
|
||||
List<User> _list = [];
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
|
@ -2,8 +2,8 @@ import 'dart:io';
|
||||
|
||||
import 'package:comunic/helpers/api_helper.dart';
|
||||
import 'package:comunic/models/api_response.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// API Request model
|
||||
@ -27,9 +27,10 @@ class BytesFile {
|
||||
class APIRequest {
|
||||
final String uri;
|
||||
final bool needLogin;
|
||||
ProgressCallback progressCallback;
|
||||
CancelToken cancelToken;
|
||||
Map<String, String> args;
|
||||
Map<String, File> files = Map();
|
||||
Map<String, PickedFile> pickedFiles = Map();
|
||||
Map<String, BytesFile> bytesFiles = Map();
|
||||
|
||||
APIRequest({@required this.uri, this.needLogin = false, this.args})
|
||||
@ -70,11 +71,6 @@ class APIRequest {
|
||||
return this;
|
||||
}
|
||||
|
||||
APIRequest addPickedFile(String name, PickedFile file) {
|
||||
pickedFiles[name] = file;
|
||||
return this;
|
||||
}
|
||||
|
||||
APIRequest addBytesFile(String name, BytesFile file) {
|
||||
this.bytesFiles[name] = file;
|
||||
return this;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:comunic/helpers/database/database_contract.dart';
|
||||
import 'package:comunic/models/cache_model.dart';
|
||||
import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
|
||||
import 'package:comunic/models/conversation_member.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';
|
||||
|
||||
/// Conversation model
|
||||
@ -10,79 +10,100 @@ import 'package:meta/meta.dart';
|
||||
|
||||
enum CallCapabilities { NONE, AUDIO, VIDEO }
|
||||
|
||||
class Conversation extends CacheModel implements Comparable {
|
||||
final int ownerID;
|
||||
final int lastActive;
|
||||
class Conversation extends SerializableElement<Conversation> {
|
||||
final int id;
|
||||
final int lastActivity;
|
||||
final String name;
|
||||
final bool following;
|
||||
final bool sawLastMessage;
|
||||
final List<int> members;
|
||||
final Color color;
|
||||
final String logoURL;
|
||||
final int groupID;
|
||||
final List<ConversationMember> members;
|
||||
final bool canEveryoneAddMembers;
|
||||
final CallCapabilities callCapabilities;
|
||||
final bool isHavingCall;
|
||||
|
||||
const Conversation({
|
||||
@required int id,
|
||||
@required this.ownerID,
|
||||
@required this.lastActive,
|
||||
Conversation({
|
||||
@required this.id,
|
||||
@required this.lastActivity,
|
||||
@required this.name,
|
||||
@required this.following,
|
||||
@required this.sawLastMessage,
|
||||
@required this.color,
|
||||
@required this.logoURL,
|
||||
@required this.groupID,
|
||||
@required this.members,
|
||||
@required this.canEveryoneAddMembers,
|
||||
this.callCapabilities = CallCapabilities.NONE,
|
||||
this.isHavingCall = false,
|
||||
}) : assert(id != null),
|
||||
assert(ownerID != null),
|
||||
assert(lastActive != null),
|
||||
assert(following != null),
|
||||
assert(sawLastMessage != null),
|
||||
assert(lastActivity != null),
|
||||
assert(members != null),
|
||||
assert(canEveryoneAddMembers != null),
|
||||
assert(callCapabilities != null),
|
||||
assert(isHavingCall != null),
|
||||
super(id: id);
|
||||
assert(isHavingCall != null);
|
||||
|
||||
/// Check out whether a conversation has a fixed name or not
|
||||
bool get hasName => this.name != null;
|
||||
|
||||
/// Check out whether current user of the application is the owner of it or
|
||||
/// not
|
||||
bool get isOwner => this.ownerID == userID();
|
||||
/// Get current user membership
|
||||
ConversationMember get membership =>
|
||||
members.firstWhere((m) => m.userID == userID());
|
||||
|
||||
Conversation.fromMap(Map<String, dynamic> map)
|
||||
: ownerID = map[ConversationTableContract.C_OWNER_ID],
|
||||
lastActive = map[ConversationTableContract.C_LAST_ACTIVE],
|
||||
name = map[ConversationTableContract.C_NAME],
|
||||
following = map[ConversationTableContract.C_FOLLOWING] == 1,
|
||||
sawLastMessage = map[ConversationTableContract.C_SAW_LAST_MESSAGE] == 1,
|
||||
members =
|
||||
listToIntList(map[ConversationTableContract.C_MEMBERS].split(",")),
|
||||
canEveryoneAddMembers =
|
||||
map[ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS] == 1,
|
||||
/// Check out whether current user of the application is an admin
|
||||
bool get isAdmin => membership.isAdmin;
|
||||
|
||||
/// Check if current user is the last admin of the conversation
|
||||
bool get isLastAdmin => isAdmin && adminsID.length == 1;
|
||||
|
||||
/// Check it current user is following the conversation or not
|
||||
bool get following => membership.following;
|
||||
|
||||
/// 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
|
||||
callCapabilities = CallCapabilities.NONE,
|
||||
isHavingCall = false,
|
||||
super.fromMap(map);
|
||||
isHavingCall = false;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
ConversationTableContract.C_ID: id,
|
||||
ConversationTableContract.C_OWNER_ID: ownerID,
|
||||
ConversationTableContract.C_LAST_ACTIVE: lastActive,
|
||||
ConversationTableContract.C_NAME: name,
|
||||
ConversationTableContract.C_FOLLOWING: following ? 1 : 0,
|
||||
ConversationTableContract.C_SAW_LAST_MESSAGE: sawLastMessage ? 1 : 0,
|
||||
ConversationTableContract.C_MEMBERS: members.join(","),
|
||||
ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS:
|
||||
canEveryoneAddMembers ? 1 : 0
|
||||
"id": id,
|
||||
"name": name,
|
||||
"color": color?.value,
|
||||
"logoURL": logoURL,
|
||||
"groupID": groupID,
|
||||
"lastActivity": lastActivity,
|
||||
"members": members.map((e) => e.toJson()).toList(),
|
||||
"canEveryoneAddMembers": canEveryoneAddMembers,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
int compareTo(other) {
|
||||
return other.lastActive.compareTo(this.lastActive);
|
||||
int compareTo(Conversation other) {
|
||||
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/models/cache_model.dart';
|
||||
import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
|
||||
import 'package:comunic/lists/users_list.dart';
|
||||
import 'package:comunic/models/displayed_content.dart';
|
||||
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';
|
||||
|
||||
/// Single conversation message
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
class ConversationMessage extends CacheModel implements Comparable {
|
||||
final int id;
|
||||
final int conversationID;
|
||||
enum ConversationMessageFileType {
|
||||
IMAGE,
|
||||
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 timeInsert;
|
||||
final DisplayedString message;
|
||||
final String imageURL;
|
||||
final int userWhoAdded;
|
||||
final int userAdded;
|
||||
final int userWhoRemoved;
|
||||
final int userRemoved;
|
||||
|
||||
const ConversationMessage({
|
||||
@required this.id,
|
||||
@required this.conversationID,
|
||||
const ConversationServerMessage({
|
||||
@required this.type,
|
||||
@required this.userID,
|
||||
@required this.timeInsert,
|
||||
@required this.message,
|
||||
@required this.imageURL,
|
||||
}) : assert(id != null),
|
||||
assert(userID != null),
|
||||
assert(timeInsert != null),
|
||||
assert(message != null),
|
||||
super(id: id);
|
||||
@required this.userWhoAdded,
|
||||
@required this.userAdded,
|
||||
@required this.userWhoRemoved,
|
||||
@required this.userRemoved,
|
||||
}) : assert(type != null),
|
||||
assert(userID != null ||
|
||||
(type != ConversationServerMessageType.USER_CREATED_CONVERSATION &&
|
||||
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 hasImage => imageURL != null && imageURL != "null";
|
||||
bool get hasFile => file != null;
|
||||
|
||||
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
|
||||
int compareTo(other) {
|
||||
int compareTo(ConversationMessage other) {
|
||||
return id.compareTo(other.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
ConversationsMessagesTableContract.C_ID: id,
|
||||
ConversationsMessagesTableContract.C_CONVERSATION_ID: conversationID,
|
||||
ConversationsMessagesTableContract.C_USER_ID: userID,
|
||||
ConversationsMessagesTableContract.C_TIME_INSERT: timeInsert,
|
||||
ConversationsMessagesTableContract.C_MESSAGE: message.content,
|
||||
ConversationsMessagesTableContract.C_IMAGE_URL: imageURL
|
||||
"id": id,
|
||||
"convID": convID,
|
||||
"userID": userID,
|
||||
"timeSent": timeSent,
|
||||
"message": message.content,
|
||||
"file": file?.toJson(),
|
||||
"serverMessage": serverMessage?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
ConversationMessage.fromMap(Map<String, dynamic> map)
|
||||
: id = map[ConversationsMessagesTableContract.C_ID],
|
||||
conversationID =
|
||||
map[ConversationsMessagesTableContract.C_CONVERSATION_ID],
|
||||
userID = map[ConversationsMessagesTableContract.C_USER_ID],
|
||||
timeInsert = map[ConversationsMessagesTableContract.C_TIME_INSERT],
|
||||
message = DisplayedString(map[ConversationsMessagesTableContract.C_MESSAGE]),
|
||||
imageURL = map[ConversationsMessagesTableContract.C_IMAGE_URL],
|
||||
super.fromMap(map);
|
||||
ConversationMessage.fromJson(Map<String, dynamic> map)
|
||||
: id = map["id"],
|
||||
convID = map["convID"],
|
||||
userID = map["userID"],
|
||||
timeSent = map["timeSent"],
|
||||
message = DisplayedString(map["message"]),
|
||||
file = map["file"] == null
|
||||
? null
|
||||
: ConversationMessageFile.fromJson(map["file"]),
|
||||
serverMessage = map["serverMessage"] == null
|
||||
? null
|
||||
: ConversationServerMessage.fromJson(map["serverMessage"]);
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class Membership {
|
||||
case MembershipType.GROUP:
|
||||
return groupLastActive;
|
||||
case MembershipType.CONVERSATION:
|
||||
return conversation.lastActive;
|
||||
return conversation.lastActivity;
|
||||
default:
|
||||
throw Exception("Unreachable statment!");
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'api_request.dart';
|
||||
|
||||
/// New comment information
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
@ -8,7 +9,7 @@ import 'package:meta/meta.dart';
|
||||
class NewComment {
|
||||
final int postID;
|
||||
final String content;
|
||||
final PickedFile image;
|
||||
final BytesFile image;
|
||||
|
||||
const NewComment({
|
||||
@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';
|
||||
|
||||
/// New conversation message model
|
||||
@ -10,14 +10,20 @@ import 'package:meta/meta.dart';
|
||||
class NewConversationMessage {
|
||||
final int conversationID;
|
||||
final String message;
|
||||
final PickedFile image;
|
||||
final BytesFile file;
|
||||
final BytesFile thumbnail;
|
||||
|
||||
NewConversationMessage(
|
||||
{@required this.conversationID, @required this.message, this.image})
|
||||
: assert(conversationID != null),
|
||||
assert(image != null || message != null);
|
||||
NewConversationMessage({
|
||||
@required this.conversationID,
|
||||
@required this.message,
|
||||
this.file,
|
||||
this.thumbnail,
|
||||
}) : assert(conversationID != null),
|
||||
assert(file != null || 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:image_picker/image_picker.dart';
|
||||
|
||||
import 'api_request.dart';
|
||||
|
||||
/// New emoji information
|
||||
///
|
||||
@ -7,7 +8,7 @@ import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class NewEmoji {
|
||||
final String shortcut;
|
||||
final PickedFile image;
|
||||
final BytesFile image;
|
||||
|
||||
const NewEmoji({
|
||||
@required this.shortcut,
|
||||
|
@ -1,9 +1,10 @@
|
||||
import 'package:comunic/enums/post_kind.dart';
|
||||
import 'package:comunic/enums/post_target.dart';
|
||||
import 'package:comunic/enums/post_visibility_level.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'api_request.dart';
|
||||
|
||||
/// New post information
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
@ -27,7 +28,7 @@ class NewPost {
|
||||
final int targetID;
|
||||
final PostVisibilityLevel visibility;
|
||||
final String content;
|
||||
final PickedFile image;
|
||||
final BytesFile image;
|
||||
final String url;
|
||||
final List<int> pdf;
|
||||
final PostKind kind;
|
||||
|
@ -57,6 +57,47 @@ class ServerDataConservationPolicy {
|
||||
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 {
|
||||
final Version minSupportedMobileVersion;
|
||||
final String termsURL;
|
||||
@ -64,6 +105,7 @@ class ServerConfig {
|
||||
final String androidDirectDownloadURL;
|
||||
final PasswordPolicy passwordPolicy;
|
||||
final ServerDataConservationPolicy dataConservationPolicy;
|
||||
final ConversationsPolicy conversationsPolicy;
|
||||
|
||||
const ServerConfig({
|
||||
@required this.minSupportedMobileVersion,
|
||||
@ -72,10 +114,12 @@ class ServerConfig {
|
||||
@required this.androidDirectDownloadURL,
|
||||
@required this.passwordPolicy,
|
||||
@required this.dataConservationPolicy,
|
||||
@required this.conversationsPolicy,
|
||||
}) : assert(minSupportedMobileVersion != null),
|
||||
assert(termsURL != null),
|
||||
assert(playStoreURL != null),
|
||||
assert(androidDirectDownloadURL != 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';
|
||||
|
||||
/// Unread conversation information
|
||||
@ -5,21 +7,12 @@ import 'package:flutter/material.dart';
|
||||
/// @author Pierre Hubert
|
||||
|
||||
class UnreadConversation {
|
||||
final int id;
|
||||
final String convName;
|
||||
final int lastActive;
|
||||
final int userID;
|
||||
final String message;
|
||||
final Conversation conv;
|
||||
final ConversationMessage message;
|
||||
|
||||
const UnreadConversation({
|
||||
@required this.id,
|
||||
@required this.convName,
|
||||
@required this.lastActive,
|
||||
@required this.userID,
|
||||
@required this.conv,
|
||||
@required this.message,
|
||||
}) : assert(id != null),
|
||||
assert(convName != null),
|
||||
assert(lastActive != null),
|
||||
assert(userID != null),
|
||||
}) : assert(conv != 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/users_helper.dart';
|
||||
import 'package:comunic/lists/users_list.dart';
|
||||
import 'package:comunic/models/conversation.dart';
|
||||
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
||||
import 'package:comunic/ui/routes/update_conversation_route.dart';
|
||||
import 'package:comunic/ui/screens/conversation_screen.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/ui_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -25,9 +29,10 @@ class ConversationRoute extends StatefulWidget {
|
||||
State<StatefulWidget> createState() => _ConversationRouteState();
|
||||
}
|
||||
|
||||
class _ConversationRouteState extends State<ConversationRoute> {
|
||||
class _ConversationRouteState extends SafeState<ConversationRoute> {
|
||||
final ConversationsHelper _conversationsHelper = ConversationsHelper();
|
||||
Conversation _conversation;
|
||||
UsersList _users;
|
||||
String _conversationName;
|
||||
bool _error = false;
|
||||
|
||||
@ -42,21 +47,22 @@ class _ConversationRouteState extends State<ConversationRoute> {
|
||||
Future<void> _loadConversation() async {
|
||||
setError(false);
|
||||
|
||||
_conversation = await _conversationsHelper.getSingle(widget.conversationID,
|
||||
force: true);
|
||||
try {
|
||||
_conversation = await _conversationsHelper
|
||||
.getSingle(widget.conversationID, force: true);
|
||||
|
||||
if (_conversation == null) return setError(true);
|
||||
_users = await UsersHelper().getList(_conversation.membersID);
|
||||
|
||||
final conversationName =
|
||||
await ConversationsHelper.getConversationNameAsync(_conversation);
|
||||
ConversationsHelper.getConversationName(_conversation, _users);
|
||||
|
||||
if (!this.mounted) return null;
|
||||
|
||||
if (conversationName == null) return setError(true);
|
||||
|
||||
setState(() {
|
||||
_conversationName = conversationName;
|
||||
});
|
||||
setState(() => _conversationName = conversationName);
|
||||
} catch (e, s) {
|
||||
print("Failed to get conversation name! $e => $s");
|
||||
setError(true);
|
||||
}
|
||||
}
|
||||
|
||||
void _openSettings() {
|
||||
@ -73,7 +79,7 @@ class _ConversationRouteState extends State<ConversationRoute> {
|
||||
return buildErrorCard(
|
||||
tr("Could not get conversation information!"),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: _loadConversation,
|
||||
child: Text(
|
||||
tr("Try again").toUpperCase(),
|
||||
@ -97,7 +103,12 @@ class _ConversationRouteState extends State<ConversationRoute> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: isTablet(context) ? null : ComunicBackButton(),
|
||||
leading: isTablet(context)
|
||||
? (_conversation == null || _users == null
|
||||
? null
|
||||
: ConversationImageWidget(
|
||||
conversation: _conversation, users: _users))
|
||||
: ComunicBackButton(),
|
||||
title: Text(
|
||||
_conversationName == null ? tr("Loading") : _conversationName,
|
||||
),
|
||||
|
@ -174,9 +174,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
|
||||
|
||||
// Submit button
|
||||
Center(
|
||||
child: RaisedButton(
|
||||
color: Colors.blue,
|
||||
textColor: Colors.white,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submitForm,
|
||||
child: Text("Submit"),
|
||||
),
|
||||
|
@ -61,7 +61,7 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> {
|
||||
|
||||
/// Step 3b - Answer security questions
|
||||
List<String> _questions;
|
||||
var _questionsControllers = List<TextEditingController>();
|
||||
var _questionsControllers = <TextEditingController>[];
|
||||
|
||||
List<String> get _answers =>
|
||||
_questionsControllers.map((f) => f.text).toList();
|
||||
@ -146,14 +146,14 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> {
|
||||
children: <Widget>[
|
||||
Text(tr("Here are your options to reset your account:")),
|
||||
_Spacer(),
|
||||
OutlineButton.icon(
|
||||
OutlinedButton.icon(
|
||||
onPressed: _openSendEmailDialog,
|
||||
icon: Icon(Icons.email),
|
||||
label: Text(tr("Send us an email to ask for help")),
|
||||
),
|
||||
_Spacer(visible: _hasSecurityQuestions),
|
||||
_hasSecurityQuestions
|
||||
? OutlineButton.icon(
|
||||
? OutlinedButton.icon(
|
||||
onPressed: _loadSecurityQuestions,
|
||||
icon: Icon(Icons.help_outline),
|
||||
label: Text(tr("Answer your security questions")),
|
||||
@ -199,7 +199,7 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> {
|
||||
..add(_Spacer())
|
||||
..addAll(List.generate(_questions.length, _buildSecurityQuestionField))
|
||||
..add(_Spacer())
|
||||
..add(OutlineButton(
|
||||
..add(OutlinedButton(
|
||||
onPressed: _canSubmitAnswers ? _submitSecurityAnswers : null,
|
||||
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:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// Full screen image details
|
||||
///
|
||||
@ -22,6 +23,10 @@ class _FullScreenImageRouteState extends State<FullScreenImageRoute> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr("Image")),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.launch), onPressed: () => launch(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),
|
||||
child: _loading
|
||||
? CircularProgressIndicator()
|
||||
: RaisedButton(
|
||||
: ElevatedButton(
|
||||
child: Text(tr("Sign in")),
|
||||
onPressed: valid ? () => _submitForm(context) : null,
|
||||
),
|
||||
|
@ -1,4 +1,7 @@
|
||||
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/main_route/page_info.dart';
|
||||
import 'package:comunic/ui/routes/settings/account_settings_route.dart';
|
||||
@ -34,7 +37,7 @@ mixin MainRoute implements StatefulWidget {}
|
||||
|
||||
/// Public interface of home controller
|
||||
abstract class MainController extends State<MainRoute> {
|
||||
final _pagesStack = List<PageInfo>();
|
||||
final _pagesStack = <PageInfo>[];
|
||||
|
||||
/// Default page of the application
|
||||
PageInfo get defaultPage;
|
||||
@ -151,6 +154,18 @@ abstract class MainController extends State<MainRoute> {
|
||||
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
|
||||
void startCall(int convID) =>
|
||||
pushPage(PageInfo(child: CallScreen(convID: convID), hideNavBar: true));
|
||||
|
@ -77,7 +77,7 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(tr("You can choose a new password.")),
|
||||
OutlineButton(
|
||||
OutlinedButton(
|
||||
onPressed: _changePassword,
|
||||
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!"),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
OutlineButton(
|
||||
OutlinedButton(
|
||||
onPressed: _quitScreen,
|
||||
child: Text(tr("Login")),
|
||||
)
|
||||
|
@ -12,9 +12,13 @@ import 'package:comunic/utils/intl_utils.dart';
|
||||
import 'package:comunic/utils/ui_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:identicon/identicon.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:random_string/random_string.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
|
||||
import '../../../utils/log_utils.dart';
|
||||
import '../../../utils/ui_utils.dart';
|
||||
|
||||
/// Account image settings section
|
||||
///
|
||||
/// @author Pierre Hubert
|
||||
@ -156,15 +160,17 @@ class _AccountImageSettingsScreenState
|
||||
|
||||
/// Upload a new account image
|
||||
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 (!await SettingsHelper.uploadAccountImage(image)) {
|
||||
showSimpleSnack(context, tr("Could not upload your account image!"));
|
||||
return;
|
||||
await SettingsHelper.uploadAccountImage(image);
|
||||
} catch (e, s) {
|
||||
logError(e, s);
|
||||
snack(context, tr("Failed to upload new account image!"));
|
||||
}
|
||||
|
||||
_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/ui_utils.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
|
||||
///
|
||||
@ -136,7 +138,7 @@ class _NewCustomEmojiDialog extends StatefulWidget {
|
||||
|
||||
class _NewCustomEmojiDialogState extends State<_NewCustomEmojiDialog> {
|
||||
final _controller = TextEditingController();
|
||||
PickedFile _file;
|
||||
BytesFile _file;
|
||||
|
||||
bool get _hasImage => _file != null;
|
||||
|
||||
@ -209,6 +211,7 @@ class _NewCustomEmojiDialogState extends State<_NewCustomEmojiDialog> {
|
||||
});
|
||||
} catch (e, 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/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';
|
||||
|
||||
/// Update a conversation route
|
||||
@ -24,70 +17,7 @@ class UpdateConversationRoute extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UpdateConversationRoute extends State<UpdateConversationRoute> {
|
||||
Conversation _conversation;
|
||||
UsersList _membersInfo;
|
||||
bool _error = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_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(),
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) =>
|
||||
UpdateConversationScreen(convID: widget.conversationID);
|
||||
}
|
||||
|
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
|
||||
_conversation =
|
||||
await ConversationsHelper().getSingleOrThrow(convID, force: true);
|
||||
await ConversationsHelper().getSingle(convID, force: true);
|
||||
_convName =
|
||||
await ConversationsHelper.getConversationNameAsync(_conversation);
|
||||
assert(_convName != null);
|
||||
@ -544,7 +544,7 @@ class _CallScreenState extends SafeState<CallScreen> {
|
||||
.where((f) => f.hasVideoStream && _renderers.containsKey(f.userID))
|
||||
.toList();
|
||||
|
||||
final rows = List<Row>();
|
||||
final rows = <Row>[];
|
||||
|
||||
var numberRows = sqrt(availableVideos.length).ceil();
|
||||
var numberCols = numberRows;
|
||||
|
@ -32,8 +32,7 @@ class _ConversationMembersScreenState extends State<ConversationMembersScreen> {
|
||||
Future<void> _refresh() async {
|
||||
_conversation =
|
||||
await ConversationsHelper().getSingle(widget.convID, force: true);
|
||||
_members =
|
||||
await UsersHelper().getListWithThrow(_conversation.members.toSet());
|
||||
_members = await UsersHelper().getListWithThrow(_conversation.membersID);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -55,12 +54,12 @@ class _ConversationMembersScreenState extends State<ConversationMembersScreen> {
|
||||
);
|
||||
|
||||
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(
|
||||
leading: AccountImageWidget(user: user),
|
||||
title: Text(user.displayName),
|
||||
subtitle:
|
||||
Text(_conversation.ownerID == user.id ? tr("Owner") : tr("Member")),
|
||||
subtitle: Text(member.isAdmin ? tr("Admin") : tr("Member")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,19 +2,34 @@ import 'dart:async';
|
||||
|
||||
import 'package:comunic/helpers/conversations_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/lists/conversation_messages_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/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/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/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/intl_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/video_utils.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:emoji_picker/emoji_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
/// Conversation screen
|
||||
///
|
||||
@ -40,15 +55,73 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
final UsersHelper _usersHelper = UsersHelper();
|
||||
|
||||
// Class members
|
||||
Conversation _conversation;
|
||||
ConversationMessagesList _messages;
|
||||
UsersList _usersInfo = UsersList();
|
||||
ErrorLevel _error = ErrorLevel.NONE;
|
||||
bool _isMessageValid = false;
|
||||
final _textFieldFocus = FocusNode();
|
||||
|
||||
bool _showEmojiPicker = false;
|
||||
|
||||
bool _isSendingMessage = false;
|
||||
TextEditingController _textEditingController = TextEditingController();
|
||||
TextEditingController _textController = TextEditingController();
|
||||
ScrollWatcher _scrollController;
|
||||
_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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -70,14 +143,16 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
});
|
||||
|
||||
/// Method called when an error occurred while loading messages
|
||||
void _errorLoading() {
|
||||
void _errorLoading() =>
|
||||
_setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR);
|
||||
}
|
||||
|
||||
/// Load the first conversations
|
||||
Future<void> _init() async {
|
||||
_scrollController = ScrollWatcher(onReachBottom: _loadOlderMessages);
|
||||
|
||||
_conversation =
|
||||
await ConversationsHelper().getSingle(widget.conversationID);
|
||||
|
||||
// Fetch latest messages
|
||||
await _loadMessages(false);
|
||||
await _loadMessages(true);
|
||||
@ -86,25 +161,42 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
.registerConversationEvents(widget.conversationID);
|
||||
|
||||
this.listen<NewConversationMessageEvent>((ev) async {
|
||||
if (ev.msg.conversationID == widget.conversationID) {
|
||||
if (ev.msg.convID == widget.conversationID) {
|
||||
try {
|
||||
await _conversationsHelper.saveMessage(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 {
|
||||
if (ev.msg.conversationID == widget.conversationID) {
|
||||
if (ev.msg.convID == widget.conversationID) {
|
||||
await _conversationsHelper.saveMessage(ev.msg);
|
||||
setState(() => _messages.replace(ev.msg));
|
||||
}
|
||||
});
|
||||
|
||||
this.listen<DeletedConversationMessageEvent>((ev) async {
|
||||
if (ev.msg.conversationID == widget.conversationID) {
|
||||
await _conversationsHelper.removeMessage(ev.msg.id);
|
||||
if (ev.msg.convID == widget.conversationID) {
|
||||
await _conversationsHelper.removeMessage(ev.msg);
|
||||
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
|
||||
@ -116,19 +208,23 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
Future<void> _loadMessages(bool online) async {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
//First, get the messages
|
||||
final messages = await _conversationsHelper.getNewMessages(
|
||||
conversationID: widget.conversationID,
|
||||
lastMessageID: _messages == null ? 0 : _messages.lastMessageID,
|
||||
online: online);
|
||||
|
||||
if (messages == null) return _errorLoading();
|
||||
online: online,
|
||||
);
|
||||
|
||||
// In case we are offline and we did not get any message we do not do
|
||||
// anything (we wait for the online request)
|
||||
if (messages.length == 0 && !online) return;
|
||||
|
||||
await _applyNewMessages(messages);
|
||||
} catch (e, s) {
|
||||
debugPrint("Failed to load messages! $e => $s", wrapWidth: 4096);
|
||||
_errorLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get older messages
|
||||
@ -136,7 +232,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
if (_loadingOlderMessages != _OlderMessagesLevel.NONE ||
|
||||
_messages == null ||
|
||||
_messages.length == 0) return;
|
||||
|
||||
try {
|
||||
// Let's start to load older messages
|
||||
_setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING);
|
||||
|
||||
@ -147,12 +243,6 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
// Mark as not loading anymore
|
||||
_setLoadingOlderMessagesState(_OlderMessagesLevel.NONE);
|
||||
|
||||
// Check for errors
|
||||
if (messages == null) {
|
||||
_errorLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there is no more unread messages
|
||||
if (messages.length == 0) {
|
||||
_setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE);
|
||||
@ -161,20 +251,24 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
|
||||
// Apply the messages
|
||||
_applyNewMessages(messages);
|
||||
} catch (e, s) {
|
||||
print("Failed to load older messages! $e => $s");
|
||||
_errorLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply new messages [messages] must not be null
|
||||
///
|
||||
/// Throws in case of failure
|
||||
Future<void> _applyNewMessages(ConversationMessagesList messages) async {
|
||||
// We ignore new messages once the area is no longer visible
|
||||
if (!this.mounted) return;
|
||||
|
||||
//Then get information about users
|
||||
final usersToGet =
|
||||
findMissingFromList(_usersInfo.usersID, messages.getUsersID());
|
||||
findMissingFromSet(_usersInfo.usersID.toSet(), messages.getUsersID());
|
||||
|
||||
final users = await _usersHelper.getUsersInfo(usersToGet);
|
||||
|
||||
if (users == null) _errorLoading();
|
||||
final users = await _usersHelper.getList(usersToGet);
|
||||
|
||||
// Save the new list of messages
|
||||
setState(() {
|
||||
@ -197,39 +291,63 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
_setError(ErrorLevel.NONE);
|
||||
}
|
||||
|
||||
/// Pick and send an image
|
||||
Future<void> _sendImage(BuildContext context) async {
|
||||
final image = await pickImage(context);
|
||||
/// Send a file message
|
||||
Future<void> _sendFileMessage() async {
|
||||
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(
|
||||
context,
|
||||
BytesFile thumbnail;
|
||||
|
||||
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(
|
||||
conversationID: widget.conversationID,
|
||||
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
|
||||
_updatedText(_textEditingController.text);
|
||||
setState(() {
|
||||
_sendCancel = null;
|
||||
_sendProgress = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Send a new text message
|
||||
Future<void> _submitTextMessage(BuildContext context, String content) async {
|
||||
if (await _submitMessage(
|
||||
context,
|
||||
NewConversationMessage(
|
||||
Future<void> _submitTextMessage() async {
|
||||
if (await _submitMessage(NewConversationMessage(
|
||||
conversationID: widget.conversationID,
|
||||
message: content,
|
||||
message: textMessage,
|
||||
)) ==
|
||||
SendMessageResult.SUCCESS) _clearSendMessageForm();
|
||||
}
|
||||
|
||||
/// Submit a new message
|
||||
Future<SendMessageResult> _submitMessage(
|
||||
BuildContext context, NewConversationMessage message) async {
|
||||
NewConversationMessage message) async {
|
||||
//Send the message
|
||||
_setSending(true);
|
||||
final result = await _conversationsHelper.sendMessage(message);
|
||||
@ -237,7 +355,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
|
||||
//Check the result of the operation
|
||||
if (result != SendMessageResult.SUCCESS)
|
||||
Scaffold.of(context).showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
result == SendMessageResult.MESSAGE_REJECTED
|
||||
@ -251,31 +369,9 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
return result;
|
||||
}
|
||||
|
||||
void _updatedText(String text) {
|
||||
setState(() {
|
||||
_isMessageValid = text.length > 2;
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear send message form
|
||||
void _clearSendMessageForm() {
|
||||
setState(() {
|
||||
_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);
|
||||
setState(() => _textController = TextEditingController());
|
||||
}
|
||||
|
||||
/// Error handling
|
||||
@ -295,7 +391,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
Widget _buildNoMessagesNotice() {
|
||||
return Expanded(
|
||||
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,
|
||||
reverse: true,
|
||||
itemCount: _messages.length,
|
||||
itemBuilder: (c, i) {
|
||||
return ConversationMessageTile(
|
||||
message: _messages.elementAt(i),
|
||||
userInfo: _usersInfo.getUser(_messages[i].userID),
|
||||
isLastMessage: _isLastMessage(i),
|
||||
isFirstMessage: _isFirstMessage(i),
|
||||
onRequestMessageUpdate: _updateMessage,
|
||||
onRequestMessageDelete: _deleteMessage,
|
||||
);
|
||||
}),
|
||||
itemBuilder: (c, i) => _buildMessageItem(i),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildMessageItem(int msgIndex) {
|
||||
final msg = _messages[msgIndex];
|
||||
final nextMessage =
|
||||
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 _buildSendMessageForm() {
|
||||
return new Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: new Row(
|
||||
Widget _buildSenderLayout(
|
||||
ConversationMessage message, ConversationMessage previousMessage) {
|
||||
final messageRadius = Radius.circular(10);
|
||||
|
||||
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>[
|
||||
// Image area
|
||||
new Container(
|
||||
margin: new EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: new IconButton(
|
||||
icon: new Icon(
|
||||
Icons.photo_camera,
|
||||
color: _isSendingMessage
|
||||
? Theme.of(context).disabledColor
|
||||
: Theme.of(context).accentColor,
|
||||
GestureDetector(
|
||||
onTap: !_isSendingMessage ? _sendFileMessage : null,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
gradient:
|
||||
_isSendingMessage ? _disabledGradient : _fabGradient,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
onPressed: () => _sendImage(context),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
// Message area
|
||||
new Flexible(
|
||||
child: new TextField(
|
||||
keyboardType: TextInputType.text,
|
||||
maxLines: null,
|
||||
maxLength: 200,
|
||||
maxLengthEnforced: true,
|
||||
|
||||
// Show max length only when there is some text already typed
|
||||
buildCounter: smartInputCounterWidgetBuilder,
|
||||
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
TextField(
|
||||
enabled: !_isSendingMessage,
|
||||
controller: _textEditingController,
|
||||
onChanged: _updatedText,
|
||||
onSubmitted: _isMessageValid
|
||||
? (s) => _submitTextMessage(context, s)
|
||||
: null,
|
||||
decoration: new InputDecoration.collapsed(
|
||||
hintText: tr("Send a message"),
|
||||
maxLines: 10,
|
||||
minLines: 1,
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocus,
|
||||
onTap: () => hideEmojiContainer(),
|
||||
textInputAction: TextInputAction.send,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Send button
|
||||
new Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: new IconButton(
|
||||
icon: new Icon(
|
||||
Icons.send,
|
||||
color: !_isSendingMessage && _isMessageValid
|
||||
? Theme.of(context).accentColor
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
onPressed: !_isSendingMessage && _isMessageValid
|
||||
? () =>
|
||||
_submitTextMessage(context, _textEditingController.text)
|
||||
IconButton(
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
if (!_showEmojiPicker) {
|
||||
// keyboard is visible
|
||||
hideKeyboard();
|
||||
Future.delayed(Duration(milliseconds: 100),
|
||||
() => showEmojiContainer());
|
||||
} else {
|
||||
//keyboard is hidden
|
||||
showKeyboard();
|
||||
hideEmojiContainer();
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.face,
|
||||
color: _showEmojiPicker
|
||||
? (_conversation.color ?? Colors.blue)
|
||||
: 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
|
||||
Widget build(BuildContext context) {
|
||||
@ -401,12 +663,34 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
: null,
|
||||
),
|
||||
_messages.length == 0 ? _buildNoMessagesNotice() : _buildMessagesList(),
|
||||
Divider(),
|
||||
_buildSendMessageForm()
|
||||
UserWritingInConvNotifier(convID: _conversation.id),
|
||||
_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
|
||||
Future<void> _updateMessage(ConversationMessage message) async {
|
||||
final newContent = await askUserString(
|
||||
@ -414,7 +698,12 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
title: tr("Update message"),
|
||||
message: tr("Please enter new 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;
|
||||
|
||||
@ -435,13 +724,13 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
TextButton(
|
||||
child: Text(
|
||||
tr("Cancel").toUpperCase(),
|
||||
),
|
||||
onPressed: () => Navigator.pop(c, false),
|
||||
),
|
||||
FlatButton(
|
||||
TextButton(
|
||||
child: Text(
|
||||
tr("Confirm").toUpperCase(),
|
||||
style: TextStyle(color: Colors.red),
|
||||
|
@ -62,32 +62,24 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
|
||||
await _loadConversationsList(false);
|
||||
}
|
||||
|
||||
void _gotLoadingError() {
|
||||
setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR);
|
||||
}
|
||||
|
||||
/// Load the list of conversations
|
||||
Future<void> _loadConversationsList(bool cached) async {
|
||||
setError(LoadErrorLevel.NONE);
|
||||
|
||||
//Get the list of conversations
|
||||
var list;
|
||||
if (cached)
|
||||
list = await _conversationsHelper.getCachedList();
|
||||
else
|
||||
list = await _conversationsHelper.downloadList();
|
||||
|
||||
if (list == null) return _gotLoadingError();
|
||||
try {
|
||||
ConversationsList list = cached
|
||||
? await _conversationsHelper.getCachedList()
|
||||
: await _conversationsHelper.downloadList();
|
||||
assert(list != null);
|
||||
|
||||
//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();
|
||||
|
||||
//Save list
|
||||
setState(() {
|
||||
_list = list;
|
||||
});
|
||||
setState(() => _list = list);
|
||||
} catch (e, s) {
|
||||
debugPrint("Failed to get conversations list! $e => $s", wrapWidth: 1024);
|
||||
setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an error card
|
||||
@ -95,7 +87,7 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
|
||||
return buildErrorCard(
|
||||
tr("Could not retrieve the list of conversations!"),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => _refreshIndicatorKey.currentState.show(),
|
||||
child: Text(
|
||||
tr("Retry").toUpperCase(),
|
||||
@ -131,37 +123,24 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
|
||||
}
|
||||
|
||||
/// Handle conversation deletion request
|
||||
Future<void> _requestDeleteConversation(Conversation conversation) async {
|
||||
final result = await showDialog<bool>(
|
||||
Future<void> _requestLeaveConversation(Conversation conversation) async {
|
||||
final result = await showConfirmDialog(
|
||||
context: context,
|
||||
builder: (c) {
|
||||
return AlertDialog(
|
||||
title: Text(tr("Delete conversation")),
|
||||
content: Text(tr(
|
||||
"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),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
message: conversation.isLastAdmin
|
||||
? tr(
|
||||
"Do you really want to leave this conversation ? As you are its last admin, it will be completely deleted!")
|
||||
: tr("Do you really want to leave this conversation ?"));
|
||||
|
||||
if (result == null || !result) return;
|
||||
|
||||
// Request the conversation to be deleted now
|
||||
if (!await _conversationsHelper.deleteConversation(conversation.id))
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(content: Text(tr("Could not delete the conversation!"))));
|
||||
try {
|
||||
await _conversationsHelper.deleteConversation(conversation.id);
|
||||
} 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
|
||||
_loadConversationsList(false);
|
||||
@ -195,7 +174,7 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
|
||||
_openConversation(c.id);
|
||||
},
|
||||
onRequestUpdate: _updateConversation,
|
||||
onRequestDelete: _requestDeleteConversation,
|
||||
onRequestLeave: _requestLeaveConversation,
|
||||
);
|
||||
},
|
||||
itemCount: _list.length,
|
||||
|
@ -1,6 +1,4 @@
|
||||
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';
|
||||
|
||||
/// Create a new conversation route
|
||||
@ -9,13 +7,5 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class CreateConversationScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: ComunicBackButton(),
|
||||
title: Text(tr("Create a conversation")),
|
||||
),
|
||||
body: UpdateConversationScreen(),
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => UpdateConversationScreen();
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ class _FriendsListScreenState extends SafeState<FriendsListScreen> {
|
||||
Widget _buildError() => buildErrorCard(
|
||||
tr("Could not load your list of friends!"),
|
||||
actions: [
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: _refreshList,
|
||||
child: Text(
|
||||
tr("Retry").toUpperCase(),
|
||||
@ -171,11 +171,11 @@ class _FriendsListScreenState extends SafeState<FriendsListScreen> {
|
||||
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!")),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(tr("Cancel").toUpperCase()),
|
||||
),
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(
|
||||
tr("Confirm").toUpperCase(),
|
||||
|
@ -259,19 +259,19 @@ class _GroupSettingsScreenState extends SafeState<GroupSettingsScreen> {
|
||||
// Upload a new logo
|
||||
SettingsTile(
|
||||
title: tr("Upload a new logo"),
|
||||
onPressed: (_) => _uploadNewLogo,
|
||||
onPressed: (_) => _uploadNewLogo(),
|
||||
),
|
||||
|
||||
// Generate a new random logo
|
||||
SettingsTile(
|
||||
title: tr("Generate a new random logo"),
|
||||
onPressed: (_) => _generateRandomLogo,
|
||||
onPressed: (_) => _generateRandomLogo(),
|
||||
),
|
||||
|
||||
// Delete current logo
|
||||
SettingsTile(
|
||||
title: tr("Delete logo"),
|
||||
onPressed: (_) => _deleteLogo,
|
||||
onPressed: (_) => _deleteLogo(),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -281,8 +281,8 @@ class _GroupSettingsScreenState extends SafeState<GroupSettingsScreen> {
|
||||
void _uploadNewLogo() async {
|
||||
try {
|
||||
final logo = await pickImage(context);
|
||||
final bytes = await logo.readAsBytes();
|
||||
await _doUploadLogo(bytes);
|
||||
if (logo == null) return;
|
||||
await _doUploadLogo(logo.bytes);
|
||||
} catch (e, stack) {
|
||||
print("Could not upload new logo! $e\n$stack");
|
||||
showSimpleSnack(context, tr("Could not upload new logo!"));
|
||||
@ -328,7 +328,7 @@ class _GroupSettingsScreenState extends SafeState<GroupSettingsScreen> {
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: tr("Delete group"),
|
||||
onPressed: (_) => _deleteGroup,
|
||||
onPressed: (_) => _deleteGroup(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -97,7 +97,7 @@ class _OtherUserFriendsListScreenState
|
||||
"Could not get the list of friends of this user !",
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
TextButton(
|
||||
child: Text(
|
||||
tr("Try again").toUpperCase(),
|
||||
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/users_list.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/conversation_image_widget.dart';
|
||||
import 'package:comunic/ui/widgets/safe_state.dart';
|
||||
import 'package:comunic/utils/date_utils.dart';
|
||||
import 'package:comunic/utils/intl_utils.dart';
|
||||
@ -71,21 +71,40 @@ class _UnreadConversationsScreenState
|
||||
|
||||
Widget _tileBuilder(BuildContext context, int 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(
|
||||
leading: AccountImageWidget(user: user),
|
||||
title: Text(user.displayName),
|
||||
leading: ConversationImageWidget(
|
||||
conversation: conv.conv,
|
||||
users: _users,
|
||||
),
|
||||
title: Text(ConversationsHelper.getConversationName(conv.conv, _users)),
|
||||
subtitle: RichText(
|
||||
text: TextSpan(style: Theme.of(context).textTheme.bodyText2, children: [
|
||||
TextSpan(text: conv.convName.isNotEmpty ? conv.convName + "\n" : ""),
|
||||
TextSpan(
|
||||
text: conv.message,
|
||||
text: messageStr,
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
]),
|
||||
),
|
||||
trailing: Text(diffTimeFromNowToStr(conv.lastActive)),
|
||||
onTap: () => MainController.of(context).openConversation(conv.id),
|
||||
trailing: Text(diffTimeFromNowToStr(conv.message.timeSent)),
|
||||
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/server_config_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/new_conversation.dart';
|
||||
import 'package:comunic/models/new_conversation_settings.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/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/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/log_utils.dart';
|
||||
import 'package:comunic/utils/ui_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Create / Update conversation screen
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
enum _MembersMenuChoices { REMOVE }
|
||||
enum _MembersMenuChoices { TOGGLE_ADMIN_STATUS, REMOVE }
|
||||
|
||||
class UpdateConversationScreen extends StatefulWidget {
|
||||
final Conversation initialSettings;
|
||||
final UsersList initialUsers;
|
||||
final convID;
|
||||
|
||||
const UpdateConversationScreen({
|
||||
Key key,
|
||||
this.initialSettings,
|
||||
this.initialUsers,
|
||||
this.convID,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -29,34 +41,82 @@ class UpdateConversationScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
||||
Conversation _conversation;
|
||||
|
||||
TextEditingController _nameController = TextEditingController();
|
||||
TextEditingController _colorController = TextEditingController();
|
||||
UsersList _members = UsersList();
|
||||
Set<int> _admins = Set();
|
||||
bool _followConversation = 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;
|
||||
|
||||
bool get _canAddMembers => isOwner || _initialSettings.canEveryoneAddMembers;
|
||||
|
||||
@override
|
||||
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;
|
||||
try {
|
||||
return HexColor(_conversationColor);
|
||||
} catch (e, s) {
|
||||
logError(e, s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
@ -66,12 +126,27 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr("Conversation name (optionnal)"),
|
||||
labelText: tr("Conversation name (optional)"),
|
||||
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 ?
|
||||
Row(
|
||||
children: <Widget>[
|
||||
@ -90,7 +165,7 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
||||
children: <Widget>[
|
||||
Switch.adaptive(
|
||||
value: _canEveryoneAddMembers,
|
||||
onChanged: isOwner
|
||||
onChanged: isAdmin
|
||||
? (b) => setState(() {
|
||||
_canEveryoneAddMembers = b;
|
||||
})
|
||||
@ -106,98 +181,219 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
|
||||
keepFocusOnChoose: true,
|
||||
label: tr("Add member"),
|
||||
enabled: _canAddMembers,
|
||||
onSelectUser: (user) => setState(() {
|
||||
if (!_members.contains(user)) _members.insert(0, user);
|
||||
}),
|
||||
),
|
||||
onSelectUser: (user) => _addMember(user)),
|
||||
|
||||
//Conversation members
|
||||
Column(
|
||||
children: _members
|
||||
.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(),
|
||||
children: _members.map((f) => _buildMemberTile(f)).toList(),
|
||||
),
|
||||
|
||||
// Submit button
|
||||
RaisedButton(
|
||||
onPressed: _members.length < 1 ? null : _submitForm,
|
||||
child: Text(isUpdating
|
||||
? tr("Update the conversation")
|
||||
: tr("Create the conversation")),
|
||||
)
|
||||
// Conversation image
|
||||
isUpdating ? _buildConversationImageWidget() : Container(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
void _membersMenuItemSelected(User user, _MembersMenuChoices choice) {
|
||||
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(() {
|
||||
_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
|
||||
Future<void> _submitForm() async {
|
||||
final settings = Conversation(
|
||||
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);
|
||||
|
||||
try {
|
||||
// Create the conversation
|
||||
var conversationID = settings.id;
|
||||
bool error = false;
|
||||
if (isUpdating)
|
||||
error = !(await ConversationsHelper().updateConversation(settings));
|
||||
else {
|
||||
conversationID = await ConversationsHelper().createConversation(settings);
|
||||
if (conversationID < 1) error = true;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (!isUpdating) {
|
||||
final conversationID = await ConversationsHelper.createConversation(
|
||||
NewConversation(
|
||||
name: _nameController.text,
|
||||
members: _members.map((element) => element.id).toList(),
|
||||
follow: _followConversation,
|
||||
canEveryoneAddMembers: _canEveryoneAddMembers,
|
||||
color: _color));
|
||||
|
||||
MainController.of(context).popPage();
|
||||
if (!isUpdating)
|
||||
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() {
|
||||
final size = MediaQuery.of(context).size;
|
||||
return Container(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
width: size.width,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
@ -99,7 +99,7 @@ class _UserPageScreenState extends SafeState<UserPageScreen> {
|
||||
body: Center(
|
||||
child:
|
||||
buildErrorCard(tr("Could not get user information!"), actions: [
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: _getUserInfo,
|
||||
child: Text(
|
||||
tr("Retry").toUpperCase(),
|
||||
|
@ -1,59 +1,90 @@
|
||||
import 'package:comunic/models/conversation_message.dart';
|
||||
import 'package:comunic/models/user.dart';
|
||||
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
||||
import 'package:comunic/ui/widgets/network_image_widget.dart';
|
||||
import 'package:comunic/ui/widgets/conversation_file_tile.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/intl_utils.dart';
|
||||
import 'package:comunic/utils/ui_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Conversation message tile
|
||||
///
|
||||
/// @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 OnRequestMessageDelete = void Function(ConversationMessage);
|
||||
|
||||
class ConversationMessageTile extends StatelessWidget {
|
||||
final ConversationMessage message;
|
||||
final User userInfo;
|
||||
final bool isLastMessage;
|
||||
final bool isFirstMessage;
|
||||
final User user;
|
||||
final OnRequestMessageStats onRequestMessageStats;
|
||||
final OnRequestMessageUpdate onRequestMessageUpdate;
|
||||
final OnRequestMessageDelete onRequestMessageDelete;
|
||||
|
||||
const ConversationMessageTile({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.userInfo,
|
||||
@required this.isLastMessage,
|
||||
@required this.isFirstMessage,
|
||||
@required this.user,
|
||||
@required this.onRequestMessageStats,
|
||||
@required this.onRequestMessageUpdate,
|
||||
@required this.onRequestMessageDelete,
|
||||
}) : assert(message != null),
|
||||
assert(userInfo != null),
|
||||
assert(isLastMessage != null),
|
||||
assert(isFirstMessage != null),
|
||||
assert(user != null),
|
||||
assert(onRequestMessageStats != null),
|
||||
assert(onRequestMessageUpdate != null),
|
||||
assert(onRequestMessageDelete != null),
|
||||
super(key: key);
|
||||
|
||||
/// Build account image
|
||||
Widget _buildAccountImage(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.all(10.0),
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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: AccountImageWidget(
|
||||
user: userInfo,
|
||||
width: 35.0,
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
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
|
||||
PopupMenuItem(
|
||||
enabled: message.isOwner,
|
||||
enabled: message.isOwner &&
|
||||
message.message != null &&
|
||||
message.message.content.isNotEmpty,
|
||||
value: _MenuChoices.REQUEST_UPDATE_CONTENT,
|
||||
child: Text(tr("Update")),
|
||||
),
|
||||
@ -64,190 +95,41 @@ class ConversationMessageTile extends StatelessWidget {
|
||||
value: _MenuChoices.DELETE,
|
||||
child: Text(tr("Delete")),
|
||||
),
|
||||
],
|
||||
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,
|
||||
]..removeWhere((element) => !element.enabled),
|
||||
),
|
||||
)
|
||||
: 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
|
||||
Container(
|
||||
child: message.hasMessage
|
||||
? Container(
|
||||
width: 200.0,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
child: TextWidget(
|
||||
Widget _buildMessageContent() {
|
||||
if (!message.hasFile)
|
||||
return TextWidget(
|
||||
content: message.message,
|
||||
textAlign: TextAlign.justify,
|
||||
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
|
||||
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);
|
||||
return ConversationFileWidget(messageID: message.id, file: message.file);
|
||||
}
|
||||
|
||||
/// Process menu choice
|
||||
void _menuOptionSelected(_MenuChoices value) {
|
||||
void _menuOptionSelected(BuildContext context, _MenuChoices 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:
|
||||
onRequestMessageUpdate(message);
|
||||
break;
|
||||
|
@ -1,10 +1,11 @@
|
||||
import 'package:comunic/helpers/conversations_helper.dart';
|
||||
import 'package:comunic/lists/users_list.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/utils/date_utils.dart';
|
||||
import 'package:comunic/utils/intl_utils.dart';
|
||||
import 'package:comunic/utils/ui_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Single conversation tile
|
||||
@ -12,17 +13,17 @@ import 'package:flutter/material.dart';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
typedef OpenConversationCallback = void Function(Conversation);
|
||||
typedef RequestDeleteConversationCallback = void Function(Conversation);
|
||||
typedef RequestLeaveConversationCallback = void Function(Conversation);
|
||||
typedef RequestUpdateConversationCallback = void Function(Conversation);
|
||||
|
||||
enum _PopupMenuChoices { UPDATE, DELETE }
|
||||
enum _PopupMenuChoices { UPDATE, LEAVE }
|
||||
|
||||
class ConversationTile extends StatelessWidget {
|
||||
final Conversation conversation;
|
||||
final UsersList usersList;
|
||||
final OpenConversationCallback onOpen;
|
||||
final RequestUpdateConversationCallback onRequestUpdate;
|
||||
final RequestDeleteConversationCallback onRequestDelete;
|
||||
final RequestLeaveConversationCallback onRequestLeave;
|
||||
|
||||
const ConversationTile({
|
||||
Key key,
|
||||
@ -30,12 +31,12 @@ class ConversationTile extends StatelessWidget {
|
||||
@required this.usersList,
|
||||
@required this.onOpen,
|
||||
@required this.onRequestUpdate,
|
||||
@required this.onRequestDelete,
|
||||
@required this.onRequestLeave,
|
||||
}) : assert(conversation != null),
|
||||
assert(usersList != null),
|
||||
assert(onOpen != null),
|
||||
assert(onRequestUpdate != null),
|
||||
assert(onRequestDelete != null),
|
||||
assert(onRequestLeave != null),
|
||||
super(key: key);
|
||||
|
||||
_buildSubInformation(IconData icon, String content) {
|
||||
@ -52,8 +53,11 @@ class ConversationTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomListTile(
|
||||
Widget build(BuildContext context) => Column(
|
||||
children: [_buildMainTile(context), _buildCallTile(context)],
|
||||
);
|
||||
|
||||
Widget _buildMainTile(BuildContext context) => CustomListTile(
|
||||
onTap: () => onOpen(conversation),
|
||||
// Conversation name
|
||||
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(
|
||||
conversation.sawLastMessage ? Icons.check_circle : Icons.lens,
|
||||
color: conversation.sawLastMessage
|
||||
? (darkTheme() ? darkAccentColor : null)
|
||||
: Colors.blue,
|
||||
),
|
||||
leading: ConversationImageWidget(
|
||||
conversation: conversation, users: usersList),
|
||||
|
||||
// Conversation information
|
||||
isThreeLine: true,
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
_buildSubInformation(
|
||||
Icons.access_time, diffTimeFromNowToStr(conversation.lastActive)),
|
||||
_buildSubInformation(Icons.access_time,
|
||||
diffTimeFromNowToStr(conversation.lastActivity)),
|
||||
_buildSubInformation(
|
||||
Icons.group,
|
||||
conversation.members.length == 1
|
||||
@ -105,12 +110,27 @@ class ConversationTile extends StatelessWidget {
|
||||
value: _PopupMenuChoices.UPDATE,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(tr("Delete")),
|
||||
value: _PopupMenuChoices.DELETE,
|
||||
child: Text(tr("Leave")),
|
||||
value: _PopupMenuChoices.LEAVE,
|
||||
)
|
||||
]).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
|
||||
@ -120,8 +140,8 @@ class ConversationTile extends StatelessWidget {
|
||||
onRequestUpdate(conversation);
|
||||
break;
|
||||
|
||||
case _PopupMenuChoices.DELETE:
|
||||
onRequestDelete(conversation);
|
||||
case _PopupMenuChoices.LEAVE:
|
||||
onRequestLeave(conversation);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -39,21 +39,25 @@ class PendingFriendTile extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
ElevatedButton(
|
||||
child: Text(
|
||||
tr("Accept").toUpperCase(),
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
color: Colors.green,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(Colors.green)),
|
||||
onPressed: () => onRespond(friend, true),
|
||||
),
|
||||
Container(width: 8.0,),
|
||||
FlatButton(
|
||||
Container(
|
||||
width: 8.0,
|
||||
),
|
||||
ElevatedButton(
|
||||
child: Text(
|
||||
tr("Reject").toUpperCase(),
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
color: Colors.red,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(Colors.red)),
|
||||
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/ui_utils.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 '../../models/api_request.dart';
|
||||
import '../../utils/log_utils.dart';
|
||||
|
||||
/// Single posts tile
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
@ -76,7 +79,7 @@ class _PostTileState extends State<PostTile> {
|
||||
|
||||
// Class members
|
||||
TextEditingController _commentController = TextEditingController();
|
||||
PickedFile _commentImage;
|
||||
BytesFile _commentImage;
|
||||
bool _submitting = false;
|
||||
int _maxNumberOfCommentToShow = 10;
|
||||
|
||||
@ -264,9 +267,9 @@ class _PostTileState extends State<PostTile> {
|
||||
}
|
||||
|
||||
Widget _buildPostYouTube() {
|
||||
return RaisedButton(
|
||||
color: Colors.red,
|
||||
textColor: Colors.white,
|
||||
return ElevatedButton(
|
||||
style:
|
||||
ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.red)),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
@ -321,7 +324,7 @@ class _PostTileState extends State<PostTile> {
|
||||
}
|
||||
|
||||
Widget _buildPostPDF() {
|
||||
return RaisedButton.icon(
|
||||
return ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
launch(widget.post.fileURL);
|
||||
},
|
||||
@ -442,8 +445,7 @@ class _PostTileState extends State<PostTile> {
|
||||
// Image button
|
||||
Container(
|
||||
width: 30,
|
||||
child: FlatButton(
|
||||
padding: EdgeInsets.only(),
|
||||
child: TextButton(
|
||||
onPressed: _pickImageForComment,
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
@ -455,8 +457,7 @@ class _PostTileState extends State<PostTile> {
|
||||
// Submit button
|
||||
Container(
|
||||
width: 40,
|
||||
child: FlatButton(
|
||||
padding: EdgeInsets.only(),
|
||||
child: TextButton(
|
||||
onPressed: _canSubmitComment ? () => _submitComment() : null,
|
||||
child: Icon(
|
||||
Icons.send,
|
||||
@ -494,11 +495,16 @@ class _PostTileState extends State<PostTile> {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Pick a new image
|
||||
final newImage = await pickImage(context);
|
||||
setState(() {
|
||||
_commentImage = newImage;
|
||||
});
|
||||
} catch (e, s) {
|
||||
logError(e, s);
|
||||
snack(context, tr("Failed to choose an image!"));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 OnUserTap onTap;
|
||||
final Widget trailing;
|
||||
final String subtitle;
|
||||
|
||||
const SimpleUserTile({Key key, this.user, this.onTap, this.trailing})
|
||||
: assert(user != null),
|
||||
const SimpleUserTile({
|
||||
Key key,
|
||||
this.user,
|
||||
this.onTap,
|
||||
this.trailing,
|
||||
this.subtitle,
|
||||
}) : assert(user != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
@ -27,6 +33,7 @@ class SimpleUserTile extends StatelessWidget {
|
||||
user: user,
|
||||
),
|
||||
title: Text(user.fullName),
|
||||
subtitle: subtitle == null ? null : Text(subtitle),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class _FriendshipStatusWidgetState extends State<FriendshipStatusWidget> {
|
||||
|
||||
// No request sent yet
|
||||
if (widget.status.noRequestExchanged) {
|
||||
return RaisedButton(
|
||||
return ElevatedButton(
|
||||
child: Text(tr("Send request").toUpperCase()),
|
||||
onPressed: () =>
|
||||
executeRequest(() => _friendsHelper.sendRequest(friendID)),
|
||||
@ -56,12 +56,13 @@ class _FriendshipStatusWidgetState extends State<FriendshipStatusWidget> {
|
||||
|
||||
// Already sent a friendship request
|
||||
if (widget.status.sentRequest) {
|
||||
return RaisedButton(
|
||||
return ElevatedButton(
|
||||
child: Text(
|
||||
tr("Cancel request").toUpperCase(),
|
||||
style: WhiteTextColorStyle,
|
||||
),
|
||||
color: Colors.red,
|
||||
style:
|
||||
ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.red)),
|
||||
onPressed: () =>
|
||||
executeRequest(() => _friendsHelper.cancelRequest(friendID)),
|
||||
);
|
||||
@ -71,21 +72,23 @@ class _FriendshipStatusWidgetState extends State<FriendshipStatusWidget> {
|
||||
if (widget.status.receivedRequest) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
ElevatedButton(
|
||||
child: Text(
|
||||
tr("Accept request").toUpperCase(),
|
||||
style: WhiteTextColorStyle,
|
||||
),
|
||||
color: Colors.green,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(Colors.green)),
|
||||
onPressed: () => executeRequest(
|
||||
() => _friendsHelper.respondRequest(friendID, true)),
|
||||
),
|
||||
RaisedButton(
|
||||
ElevatedButton(
|
||||
child: Text(
|
||||
tr("Reject request").toUpperCase(),
|
||||
style: WhiteTextColorStyle,
|
||||
),
|
||||
color: Colors.red,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(Colors.red)),
|
||||
onPressed: () => executeRequest(
|
||||
() => _friendsHelper.respondRequest(friendID, false)),
|
||||
)
|
||||
@ -94,7 +97,7 @@ class _FriendshipStatusWidgetState extends State<FriendshipStatusWidget> {
|
||||
}
|
||||
|
||||
// The two users are friends, offers to follow him
|
||||
return RaisedButton(
|
||||
return ElevatedButton(
|
||||
child: Text((widget.status.following ? tr("Following") : tr("Follow"))
|
||||
.toUpperCase()),
|
||||
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 GestureLongPressCallback onLongPress;
|
||||
final bool selected;
|
||||
final Color tileColor;
|
||||
|
||||
/// Custom onLongPress function
|
||||
final Function(Size, Offset) onLongPressWithInfo;
|
||||
@ -39,6 +40,7 @@ class CustomListTile extends StatelessWidget {
|
||||
this.selected = false,
|
||||
this.onLongPressWithInfo,
|
||||
this.onLongPressOpenMenu,
|
||||
this.tileColor,
|
||||
}) : assert(isThreeLine != null),
|
||||
assert(enabled != null),
|
||||
assert(selected != null),
|
||||
@ -48,6 +50,7 @@ class CustomListTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
tileColor: tileColor,
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
|
@ -153,8 +153,7 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
|
||||
SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
RaisedButton(
|
||||
color: Colors.indigo,
|
||||
ElevatedButton(
|
||||
onPressed: () => _tryConnect(),
|
||||
child: Text(tr("Try again")),
|
||||
),
|
||||
|
@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class NetworkImageWidget extends StatelessWidget {
|
||||
final String url;
|
||||
final String thumbnailURL;
|
||||
final bool allowFullScreen;
|
||||
final bool roundedEdges;
|
||||
final double width;
|
||||
@ -19,6 +20,7 @@ class NetworkImageWidget extends StatelessWidget {
|
||||
const NetworkImageWidget({
|
||||
Key key,
|
||||
@required this.url,
|
||||
this.thumbnailURL,
|
||||
this.allowFullScreen = false,
|
||||
this.width,
|
||||
this.height,
|
||||
@ -42,7 +44,7 @@ class NetworkImageWidget extends StatelessWidget {
|
||||
}
|
||||
: null,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
imageUrl: thumbnailURL ?? url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.cover,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:comunic/enums/post_kind.dart';
|
||||
import 'package:comunic/enums/post_target.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/post_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: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
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
const _ActiveButtonsColor = Colors.blue;
|
||||
const _ActiveButtonsTextColor = Colors.white;
|
||||
const _InactiveButtonsColor = Colors.grey;
|
||||
const _InactiveButtonsTextColor = Colors.black;
|
||||
|
||||
class PostCreateFormWidget extends StatefulWidget {
|
||||
final PostTarget postTarget;
|
||||
@ -51,7 +51,7 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
||||
bool _isCreating = false;
|
||||
final TextEditingController _postTextController = TextEditingController();
|
||||
PostVisibilityLevel _postVisibilityLevel;
|
||||
PickedFile _postImage;
|
||||
BytesFile _postImage;
|
||||
String _postURL;
|
||||
List<int> _postPDF;
|
||||
DateTime _timeEnd;
|
||||
@ -193,14 +193,9 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
||||
// Submit post button
|
||||
_isCreating
|
||||
? Container()
|
||||
: FlatButton(
|
||||
: ElevatedButton(
|
||||
child: Text(tr("Send").toUpperCase()),
|
||||
onPressed: canSubmitForm ? _submitForm : null,
|
||||
color: _ActiveButtonsColor,
|
||||
textColor: _ActiveButtonsTextColor,
|
||||
disabledColor: _InactiveButtonsColor,
|
||||
disabledTextColor: _InactiveButtonsTextColor,
|
||||
),
|
||||
onPressed: canSubmitForm ? _submitForm : null),
|
||||
],
|
||||
),
|
||||
)
|
||||
@ -247,6 +242,7 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
||||
|
||||
/// Pick an image for the new post
|
||||
Future<void> _pickImageForPost() async {
|
||||
try {
|
||||
final image = await pickImage(context);
|
||||
|
||||
if (image == null) return;
|
||||
@ -256,6 +252,10 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
||||
setState(() {
|
||||
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
|
||||
@ -278,15 +278,18 @@ class _PostCreateFormWidgetState extends State<PostCreateFormWidget> {
|
||||
/// Pick a PDF for the new post
|
||||
Future<void> _pickPDFForPost() async {
|
||||
try {
|
||||
final picker = await FilePickerCross.importFromStorage(
|
||||
type: FileTypeCross.custom,
|
||||
fileExtension: "pdf",
|
||||
final file = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ["pdf"],
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (file == null || file.files.isEmpty) return;
|
||||
|
||||
_resetPostSelection();
|
||||
|
||||
setState(() {
|
||||
this._postPDF = picker.toUint8List();
|
||||
this._postPDF = file.files.first.bytes;
|
||||
});
|
||||
} catch (e, stack) {
|
||||
print("Pick PDF error: $e\n$stack");
|
||||
|
@ -119,6 +119,7 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
|
||||
|
||||
_loading = true;
|
||||
|
||||
try {
|
||||
final list = !getOlder
|
||||
? await widget.getPostsList()
|
||||
: await widget.getOlder(_list.oldestID);
|
||||
@ -127,8 +128,6 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
|
||||
|
||||
final users = await _usersHelper.getList(list.usersID);
|
||||
|
||||
if (users == null) return _loadError();
|
||||
|
||||
final groups = await _groupsHelper.getList(list.groupsID);
|
||||
|
||||
if (groups == null) return _loadError();
|
||||
@ -146,6 +145,10 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
|
||||
_groups.addAll(groups);
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
print("Failed to load post information ! $e => $s");
|
||||
_loadError();
|
||||
}
|
||||
|
||||
_loading = false;
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ import 'package:flutter/material.dart';
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
abstract class SafeState<T extends StatefulWidget> extends State<T> {
|
||||
final _subscriptions = List<StreamSubscription>();
|
||||
final _timers = List<Timer>();
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
final _timers = <Timer>[];
|
||||
|
||||
bool _unmounted = false;
|
||||
|
||||
|
@ -81,7 +81,7 @@ class _ConversationWindowState extends SafeState<ConversationWindow> {
|
||||
_refresh();
|
||||
|
||||
listen<NewConversationMessageEvent>((e) {
|
||||
if (e.msg.conversationID == _convID &&
|
||||
if (e.msg.convID == _convID &&
|
||||
_collapsed &&
|
||||
e.msg.userID != userID()) setState(() => _hasNewMessages = true);
|
||||
});
|
||||
@ -99,8 +99,7 @@ class _ConversationWindowState extends SafeState<ConversationWindow> {
|
||||
isCollapsed: _collapsed,
|
||||
body: buildErrorCard(tr("Could not load conversation information!"),
|
||||
actions: [
|
||||
FlatButton(
|
||||
textColor: Colors.white,
|
||||
ElevatedButton(
|
||||
onPressed: _refresh,
|
||||
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/page_info.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_membership_widget.dart';
|
||||
import 'package:comunic/ui/widgets/safe_state.dart';
|
||||
@ -202,9 +203,21 @@ class _MembershipsPanelState extends SafeState<MembershipsPanel> {
|
||||
color: color,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: Icon(Icons.message),
|
||||
title: Text(
|
||||
ConversationsHelper.getConversationName(conversation, _usersList)),
|
||||
leading: ConversationImageWidget(
|
||||
conversation: conversation,
|
||||
users: _usersList,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.message,
|
||||
size: 12,
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Text(ConversationsHelper.getConversationName(
|
||||
conversation, _usersList)),
|
||||
],
|
||||
),
|
||||
subtitle: Text(diffTimeFromNowToStr(membership.lastActive) +
|
||||
(conversation.isHavingCall ? "\n" + tr("Ongoing call") : "")),
|
||||
onTap: () => MainController.of(context)
|
||||
|
@ -123,7 +123,7 @@ class _UserPageTabletState extends State<UserPageTablet> {
|
||||
// Friends list of the user
|
||||
_userInfo.isFriendsListPublic
|
||||
? Expanded(
|
||||
child: OutlineButton.icon(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => MainController.of(context)
|
||||
.openUserFriendsList(_userInfo.id),
|
||||
icon: Icon(Icons.group),
|
||||
@ -135,7 +135,7 @@ class _UserPageTabletState extends State<UserPageTablet> {
|
||||
// Private messages
|
||||
!_isCurrentUser
|
||||
? Expanded(
|
||||
child: OutlineButton(
|
||||
child: OutlinedButton(
|
||||
onPressed: () =>
|
||||
openPrivateConversation(context, _userInfo.id),
|
||||
child: Icon(Icons.message),
|
||||
|
@ -27,7 +27,7 @@ class TextRichContentWidget extends StatelessWidget {
|
||||
static List<TextSpan> _parse(String text, TextStyle style) {
|
||||
if (style == null) style = TextStyle();
|
||||
|
||||
List<TextSpan> list = List();
|
||||
List<TextSpan> list = [];
|
||||
String currString = "";
|
||||
|
||||
text.split("\n").forEach((f) {
|
||||
|
@ -55,7 +55,7 @@ class TextWidget extends StatelessWidget {
|
||||
List<InlineSpan> _parseLinks(
|
||||
BuildContext context, String text, TextStyle style) {
|
||||
var buff = StringBuffer();
|
||||
final list = new List<InlineSpan>();
|
||||
final list = <InlineSpan>[];
|
||||
|
||||
// Change word function
|
||||
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 !!!
|
||||
String text;
|
||||
final _ElementStyle style;
|
||||
final List<_Element> children = List();
|
||||
final List<_Element> children = [];
|
||||
|
||||
_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]
|
||||
Future<bool> openPrivateConversation(BuildContext context, int userID) async {
|
||||
try {
|
||||
final convID = await ConversationsHelper().getPrivate(userID);
|
||||
|
||||
if (convID == null) {
|
||||
showSimpleSnack(context, tr("Could not find a private conversation!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open the conversation
|
||||
MainController.of(context).openConversation(convID);
|
||||
|
||||
// Success
|
||||
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