diff options
author | Iván Ávalos <avalos@disroot.org> | 2023-05-22 00:09:43 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2023-05-22 00:09:43 -0600 |
commit | 786267d0ebb337ad5b4f1e528fdd4c23731e0606 (patch) | |
tree | 6e696aeae7fe14244b555da9815ecd19d0b92b14 | |
parent | 41b1ed94d6964e3fcedf427bbd323861b03fd1de (diff) | |
download | linkchat-786267d0ebb337ad5b4f1e528fdd4c23731e0606.tar.gz linkchat-786267d0ebb337ad5b4f1e528fdd4c23731e0606.tar.bz2 linkchat-786267d0ebb337ad5b4f1e528fdd4c23731e0606.zip |
Se implementa funcionalidad básica de chat
-rw-r--r-- | lib/firebase/auth.dart | 16 | ||||
-rw-r--r-- | lib/firebase/database.dart | 74 | ||||
-rw-r--r-- | lib/models/chat.dart | 26 | ||||
-rw-r--r-- | lib/models/favorito.dart | 16 | ||||
-rw-r--r-- | lib/models/group.dart | 46 | ||||
-rw-r--r-- | lib/models/mensaje.dart | 30 | ||||
-rw-r--r-- | lib/models/message.dart | 29 | ||||
-rw-r--r-- | lib/models/user.dart | 25 | ||||
-rw-r--r-- | lib/routes.dart | 6 | ||||
-rw-r--r-- | lib/screens/chat_screen.dart | 97 | ||||
-rw-r--r-- | lib/screens/dashboard_screen.dart | 6 | ||||
-rw-r--r-- | lib/screens/new_chat_screen.dart | 99 | ||||
-rw-r--r-- | lib/widgets/active_chats.dart | 42 | ||||
-rw-r--r-- | lib/widgets/chat_bottom_sheet.dart | 52 | ||||
-rw-r--r-- | lib/widgets/chat_bubble.dart | 83 | ||||
-rw-r--r-- | lib/widgets/chat_item.dart | 113 | ||||
-rw-r--r-- | lib/widgets/recent_chats.dart | 87 | ||||
-rw-r--r-- | pubspec.lock | 16 | ||||
-rw-r--r-- | pubspec.yaml | 2 |
19 files changed, 788 insertions, 77 deletions
diff --git a/lib/firebase/auth.dart b/lib/firebase/auth.dart index a876d5b..303412e 100644 --- a/lib/firebase/auth.dart +++ b/lib/firebase/auth.dart @@ -2,7 +2,9 @@ import 'dart:io'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; +import 'package:linkchat/firebase/database.dart'; +import '../models/user.dart'; import 'storage.dart'; class Auth { @@ -10,6 +12,8 @@ class Auth { final GithubAuthProvider _githubProvider = GithubAuthProvider(); final GoogleAuthProvider _googleAuthProvider = GoogleAuthProvider(); + final Database _db = Database(); + User? get currentUser => _auth.currentUser; Future<bool> createUserWithEmailAndPassword({ @@ -25,8 +29,16 @@ class Auth { ); User? user = cred.user; if (user != null) { - user.updateDisplayName(displayName); - user.updatePhotoURL(await Storage().uploadAvatar(user.uid, avatar)); + String photoUrl = await Storage().uploadAvatar(user.uid, avatar); + await user.updateDisplayName(displayName); + await user.updatePhotoURL(photoUrl); + // Store user in database + _db.saveUser(FsUser( + uid: user.uid, + displayName: displayName, + photoUrl: photoUrl, + email: user.email!, + )); } return true; } catch (e) { diff --git a/lib/firebase/database.dart b/lib/firebase/database.dart new file mode 100644 index 0000000..eb9ef05 --- /dev/null +++ b/lib/firebase/database.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:linkchat/models/group.dart'; +import 'package:linkchat/models/message.dart'; + +import '../models/user.dart'; + +class Database { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + Future<FsUser?> getUserById(String uid) async { + var snap = await _firestore.collection('users').doc(uid).get(); + return snap.data() != null ? FsUser.fromMap(snap.data()!) : null; + } + + Stream<List<FsUser>> getAllUsers() { + return _firestore.collection('users').snapshots().map<List<FsUser>>((e) { + return e.docs.map((e) { + return FsUser.fromMap(e.data()); + }).toList(); + }); + } + + Future<void> saveUser(FsUser user) async { + await _firestore.collection('users').doc(user.uid).set({ + "uid": user.uid, + "displayName": user.displayName, + "photoUrl": user.photoUrl, + "email": user.email, + }); + } + + Stream<List<Group>> getGroupsByUserID(String uid) { + return _firestore + .collection('groups') + .where('members', arrayContains: uid) + .snapshots() + .map<List<Group>>((e) { + return e.docs.map((e) { + return Group.fromMap(e.data(), e.id); + }).toList(); + }); + } + + Stream<List<Message>> getMessagesByGroupId(String id) { + return _firestore + .collection('messages') + .doc(id) + .collection('messages') + .orderBy('sentAt') + .snapshots() + .map<List<Message>>((e) { + return e.docs.map((e) { + return Message.fromMap(e.data()); + }).toList(); + }); + } + + Future<void> saveMessage(Message msg, String groupId) async { + await _firestore + .collection('messages') + .doc(groupId) + .collection('messages') + .add(msg.toMap()); + await _firestore.collection('groups').doc(groupId).update({ + "recentMessage": msg.toMap(), + }); + } + + Future<void> saveGroup(Group group) async { + await _firestore.collection('groups').add(group.toMap()); + } +} diff --git a/lib/models/chat.dart b/lib/models/chat.dart deleted file mode 100644 index 6bd4752..0000000 --- a/lib/models/chat.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:linkchat/models/mensaje.dart'; - -class Chat { - final String id; - final String idUsuario1; - final String idUsuario2; - final List<Mensaje> mensajes; - - const Chat({ - required this.id, - required this.idUsuario1, - required this.idUsuario2, - required this.mensajes, - }); - - factory Chat.fromMap(Map<String, dynamic> map) { - return Chat( - id: map['id'], - idUsuario1: map['usuario1_id'], - idUsuario2: map['usuario2_id'], - mensajes: (map['mensajes'] as List<Map<String, dynamic>>) - .map((msj) => Mensaje.fromMap(msj)) - .toList(), - ); - } -} diff --git a/lib/models/favorito.dart b/lib/models/favorito.dart deleted file mode 100644 index 64422be..0000000 --- a/lib/models/favorito.dart +++ /dev/null @@ -1,16 +0,0 @@ -class Favorito { - final String chatId; - final String mensajeId; - - const Favorito({ - required this.chatId, - required this.mensajeId, - }); - - factory Favorito.fromMap(Map<String, dynamic> map) { - return Favorito( - chatId: map['chat_id'], - mensajeId: map['mensaje_id'], - ); - } -} diff --git a/lib/models/group.dart b/lib/models/group.dart new file mode 100644 index 0000000..dedb103 --- /dev/null +++ b/lib/models/group.dart @@ -0,0 +1,46 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +import 'message.dart'; + +class Group { + final String? id; + final String? name; + final Message? recentMessage; + final List<String> members; + final String createdBy; + final DateTime createdAt; + + const Group({ + this.id, + this.name, + this.recentMessage, + this.members = const [], + required this.createdBy, + required this.createdAt, + }); + + Map<String, dynamic> toMap() { + return { + "id": id, + "name": name, + "recentMessage": recentMessage, + "members": members, + "createdBy": createdBy, + "createdAt": createdAt, + }; + } + + factory Group.fromMap(Map<String, dynamic> map, String id) { + List<dynamic> members = map['members']; + return Group( + id: id, + name: map['name'], + recentMessage: map['recentMessage'] != null + ? Message.fromMap(map['recentMessage']) + : null, + members: members.map((m) => m.toString()).toList(), + createdBy: map['createdBy'], + createdAt: (map['createdAt'] as Timestamp).toDate(), + ); + } +} diff --git a/lib/models/mensaje.dart b/lib/models/mensaje.dart deleted file mode 100644 index c2f4354..0000000 --- a/lib/models/mensaje.dart +++ /dev/null @@ -1,30 +0,0 @@ -enum Direccion { a2b, b2a } - -class Mensaje { - final String id; - final String link; - final String? titulo; - final String? imagen; - final String fecha; - final Direccion direccion; - - const Mensaje({ - required this.id, - required this.link, - this.titulo, - this.imagen, - required this.fecha, - required this.direccion, - }); - - factory Mensaje.fromMap(Map<String, dynamic> map) { - return Mensaje( - id: map['id'], - link: map['link'], - titulo: map.containsKey('titulo') ? map['titulo'] : null, - imagen: map.containsKey('imagen') ? map['imagen'] : null, - fecha: map['fecha'], - direccion: (map['fecha'] as int) == 0 ? Direccion.a2b : Direccion.b2a, - ); - } -} diff --git a/lib/models/message.dart b/lib/models/message.dart new file mode 100644 index 0000000..ac31c18 --- /dev/null +++ b/lib/models/message.dart @@ -0,0 +1,29 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class Message { + final String messageText; + final DateTime sentAt; + final String sentBy; + + const Message({ + required this.messageText, + required this.sentAt, + required this.sentBy, + }); + + Map<String, dynamic> toMap() { + return { + "messageText": messageText, + "sentAt": sentAt, + "sentBy": sentBy, + }; + } + + factory Message.fromMap(Map<String, dynamic> map) { + return Message( + messageText: map['messageText'], + sentAt: (map['sentAt'] as Timestamp).toDate(), + sentBy: map['sentBy'], + ); + } +} diff --git a/lib/models/user.dart b/lib/models/user.dart new file mode 100644 index 0000000..1827552 --- /dev/null +++ b/lib/models/user.dart @@ -0,0 +1,25 @@ +class FsUser { + final String uid; + final String displayName; + final String photoUrl; + final String email; + final List<String> groups; + + const FsUser({ + required this.uid, + required this.displayName, + required this.photoUrl, + required this.email, + this.groups = const [], + }); + + factory FsUser.fromMap(Map<String, dynamic> map) { + return FsUser( + uid: map['uid'], + displayName: map['displayName'], + photoUrl: map['photoUrl'], + email: map['email'], + groups: map['groups'] ?? [], + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 9446a01..a2ad16e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:linkchat/screens/chat_screen.dart'; +import 'package:linkchat/screens/new_chat_screen.dart'; import 'screens/dashboard_screen.dart'; import 'screens/login_screen.dart'; @@ -10,6 +12,8 @@ Map<String, WidgetBuilder> getApplicationRoutes() { '/login': (BuildContext context) => const LoginScreen(), '/register': (BuildContext context) => const RegisterScreen(), '/onboard': (BuildContext context) => const OnboardingScreen(), - '/dash': (BuildContext context) => const DashboardScreen() + '/dash': (BuildContext context) => const DashboardScreen(), + '/new': (BuildContext context) => const NewChatScreen(), + '/chat': (BuildContext context) => const ChatScreen(), }; } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart new file mode 100644 index 0000000..d67e469 --- /dev/null +++ b/lib/screens/chat_screen.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:linkchat/firebase/auth.dart'; +import 'package:linkchat/firebase/database.dart'; +import 'package:linkchat/models/group.dart'; + +import '../models/message.dart'; +import '../models/user.dart'; +import '../widgets/chat_bottom_sheet.dart'; +import '../widgets/chat_bubble.dart'; + +class ChatScreen extends StatefulWidget { + const ChatScreen({super.key}); + + @override + State<ChatScreen> createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State<ChatScreen> { + bool init = false; + Group? group; + FsUser? user; + final Auth _auth = Auth(); + final Database _db = Database(); + + @override + Widget build(BuildContext context) { + List<dynamic> arguments = + ModalRoute.of(context)?.settings.arguments as List<dynamic>; + group = arguments[0] as Group; + user = arguments[1] as FsUser; + + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(70.0), + child: Padding( + padding: const EdgeInsets.only(top: 5), + child: AppBar( + leadingWidth: 30, + title: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(30), + child: CircleAvatar( + backgroundImage: NetworkImage(user!.photoUrl), + )), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text(user!.displayName), + ), + ], + ), + actions: const [ + Padding( + padding: EdgeInsets.only(right: 10), + child: Icon( + Icons.more_vert, + ), + ) + ], + ), + ), + ), + body: StreamBuilder( + stream: _db.getMessagesByGroupId(group!.id!), + builder: (context, snapshot) { + if (snapshot.hasData) { + List<Message> msgs = snapshot.data!; + return ListView.builder( + itemCount: snapshot.data!.length, + itemBuilder: (context, index) => ChatBubble( + msgs[index].messageText, + alignment: msgs[index].sentBy == _auth.currentUser?.uid + ? ChatBubbleAlignment.end + : ChatBubbleAlignment.start, + ), + ); + } else if (snapshot.hasError) { + print('Error: ${snapshot.error}'); + return const Center(child: Text('Hubo un error')); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + bottomSheet: ChatBottomSheet( + onPressed: (value) { + _db.saveMessage( + Message( + messageText: value, + sentBy: _auth.currentUser!.uid, + sentAt: DateTime.now(), + ), + group!.id!); + }, + ), + ); + } +} diff --git a/lib/screens/dashboard_screen.dart b/lib/screens/dashboard_screen.dart index cb31a58..778c2bb 100644 --- a/lib/screens/dashboard_screen.dart +++ b/lib/screens/dashboard_screen.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../firebase/auth.dart'; import '../providers/theme_provider.dart'; import '../settings/themes.dart'; +import '../widgets/recent_chats.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @@ -30,8 +31,9 @@ class _DashboardScreenState extends State<DashboardScreen> { SliverAppBar.large( title: const Text('Inicio'), ), - const SliverFillRemaining( - child: Placeholder(), + SliverFillRemaining( + hasScrollBody: true, + child: RecentChats(), ), ], ), diff --git a/lib/screens/new_chat_screen.dart b/lib/screens/new_chat_screen.dart new file mode 100644 index 0000000..7b7721f --- /dev/null +++ b/lib/screens/new_chat_screen.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:linkchat/firebase/database.dart'; +import 'package:linkchat/models/group.dart'; + +import '../firebase/auth.dart'; +import '../models/user.dart'; + +class NewChatScreen extends StatefulWidget { + const NewChatScreen({super.key}); + + @override + State<NewChatScreen> createState() => _NewChatScreenState(); +} + +class _NewChatScreenState extends State<NewChatScreen> { + final Auth _auth = Auth(); + final Database _db = Database(); + final TextEditingController _controller = TextEditingController(); + List<FsUser> users = []; + List<FsUser> filteredUsers = []; + + @override + void initState() { + super.initState(); + _db.getAllUsers().first.then((u) { + setState(() { + users = u; + filteredUsers = u; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Nuevo chat'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: Column( + children: [ + TextField( + controller: _controller, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Nombre del contacto', + ), + onChanged: (value) { + setState(() { + if (value.isNotEmpty) { + filteredUsers = users + .where((user) => user.displayName.contains(value)) + .toList(); + } else { + filteredUsers = users; + } + }); + }, + ), + ListView.builder( + shrinkWrap: true, + itemCount: filteredUsers.length, + itemBuilder: (context, index) { + FsUser user = filteredUsers[index]; + return ListTile( + leading: CircleAvatar( + backgroundImage: NetworkImage(user.photoUrl), + ), + title: Text(user.displayName), + trailing: IconButton( + icon: const Icon(Icons.send), + onPressed: () { + _db + .saveGroup(Group( + createdBy: _auth.currentUser!.uid, + createdAt: DateTime.now(), + members: [ + _auth.currentUser!.uid, + user.uid, + ], + )) + .whenComplete(() { + Navigator.of(context).pop(); + }); + }, + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/active_chats.dart b/lib/widgets/active_chats.dart new file mode 100644 index 0000000..6893edb --- /dev/null +++ b/lib/widgets/active_chats.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart';
+
+class ActiveChats extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 25, left: 5),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: [
+ for (int i = 0; i < 10; i++)
+ Padding(
+ padding:
+ const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
+ child: Container(
+ width: 55,
+ height: 55,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(35),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.grey.withOpacity(0.5),
+ blurRadius: 10,
+ spreadRadius: 2,
+ offset: const Offset(0, 3),
+ ),
+ ]),
+ child: const ClipRRect(
+ //child: Image.asset(
+ // "images/avatar.png",
+ //),
+ child: Icon(Icons.person),
+ ),
+ ),
+ ),
+ ],
+ )),
+ );
+ }
+}
diff --git a/lib/widgets/chat_bottom_sheet.dart b/lib/widgets/chat_bottom_sheet.dart new file mode 100644 index 0000000..f1124cf --- /dev/null +++ b/lib/widgets/chat_bottom_sheet.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart';
+
+class ChatBottomSheet extends StatelessWidget {
+ final Function(String msg) onPressed;
+ final TextEditingController _controller = TextEditingController();
+
+ ChatBottomSheet({super.key, required this.onPressed});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 55,
+ decoration: BoxDecoration(boxShadow: [
+ BoxShadow(
+ color: Colors.grey.withOpacity(0.5),
+ spreadRadius: 2,
+ blurRadius: 10,
+ offset: const Offset(0, 3),
+ ),
+ ]),
+ child: Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 10),
+ child: Container(
+ alignment: Alignment.centerRight,
+ width: 270,
+ child: TextFormField(
+ controller: _controller,
+ decoration: const InputDecoration(
+ hintText: "Escribe algo",
+ border: InputBorder.none,
+ ),
+ ),
+ ),
+ ),
+ const Spacer(),
+ Padding(
+ padding: const EdgeInsets.only(right: 10),
+ child: IconButton(
+ icon: const Icon(Icons.send),
+ onPressed: () {
+ onPressed(_controller.value.text);
+ _controller.clear();
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/chat_bubble.dart b/lib/widgets/chat_bubble.dart new file mode 100644 index 0000000..aaac93d --- /dev/null +++ b/lib/widgets/chat_bubble.dart @@ -0,0 +1,83 @@ +import 'package:custom_clippers/custom_clippers.dart';
+import 'package:flutter/material.dart';
+
+enum ChatBubbleAlignment { start, end }
+
+class ChatBubble extends StatelessWidget {
+ final String text;
+ final ChatBubbleAlignment alignment;
+
+ const ChatBubble(
+ this.text, {
+ super.key,
+ required this.alignment,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ alignment == ChatBubbleAlignment.start
+ ? BubbleLeft(text)
+ : BubbleRight(text),
+ ],
+ );
+ }
+}
+
+class BubbleLeft extends StatelessWidget {
+ final String text;
+
+ const BubbleLeft(this.text, {super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(right: 77),
+ child: ClipPath(
+ clipper: UpperNipMessageClipper(MessageType.receive),
+ child: Container(
+ padding: const EdgeInsets.all(20),
+ decoration: const BoxDecoration(
+ color: Color(0xFFE1E1E2),
+ ),
+ child: Text(
+ text,
+ style: const TextStyle(fontSize: 15, color: Colors.black),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class BubbleRight extends StatelessWidget {
+ final String text;
+
+ const BubbleRight(this.text, {super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ alignment: Alignment.centerRight,
+ child: Padding(
+ padding: const EdgeInsets.only(top: 20, left: 77),
+ child: ClipPath(
+ clipper: LowerNipMessageClipper(MessageType.send),
+ child: Container(
+ padding:
+ const EdgeInsets.only(left: 20, top: 10, bottom: 20, right: 20),
+ decoration: const BoxDecoration(
+ color: Color(0xFF113753),
+ ),
+ child: Text(
+ text,
+ style: const TextStyle(fontSize: 15, color: Colors.white),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/chat_item.dart b/lib/widgets/chat_item.dart new file mode 100644 index 0000000..d04c607 --- /dev/null +++ b/lib/widgets/chat_item.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class ChatItem extends StatelessWidget { + final String title; + final String subtitle; + final DateTime timestamp; + final int? unread; + final String? avatarURL; + final Function() onTap; + + const ChatItem({ + super.key, + required this.title, + required this.subtitle, + required this.onTap, + required this.timestamp, + this.unread = 0, + this.avatarURL, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 15), + child: InkWell( + onTap: onTap, + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(35), + child: CircleAvatar( + backgroundColor: Theme.of(context).colorScheme.primary, + backgroundImage: + avatarURL != null ? NetworkImage(avatarURL!) : null, + child: avatarURL == null + ? Icon( + Icons.person, + color: Theme.of(context).colorScheme.onPrimary, + ) + : null, + )), + Expanded( + flex: 12, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 17, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + Text( + subtitle, + style: const TextStyle( + fontSize: 17, + color: Colors.black54, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(right: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + DateFormat('kk:mm').format(timestamp), + style: const TextStyle( + fontSize: 15, + color: Colors.black54, + ), + ), + const SizedBox(height: 10), + unread != null && unread != 0 + ? Container( + height: 23, + width: 23, + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0xFF113753), + borderRadius: BorderRadius.circular(25), + ), + child: Text( + unread!.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.bold, + ), + ), + ) + : const SizedBox.shrink(), + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/widgets/recent_chats.dart b/lib/widgets/recent_chats.dart new file mode 100644 index 0000000..02d4959 --- /dev/null +++ b/lib/widgets/recent_chats.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart';
+import 'package:linkchat/firebase/database.dart';
+
+import '../firebase/auth.dart';
+import '../models/group.dart';
+import '../widgets/chat_item.dart';
+
+class RecentChats extends StatelessWidget {
+ RecentChats({super.key});
+
+ final Auth _auth = Auth();
+ final Database _db = Database();
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.only(top: 20),
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 25),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(35),
+ topRight: Radius.circular(35),
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.grey.withOpacity(0.5),
+ blurRadius: 10,
+ spreadRadius: 2,
+ offset: const Offset(0, 2),
+ ),
+ ]),
+ child: StreamBuilder(
+ stream: _db.getGroupsByUserID(_auth.currentUser!.uid),
+ builder: (context, snapshot) {
+ if (snapshot.hasData) {
+ return ListView.builder(
+ itemCount: snapshot.data!.length,
+ itemBuilder: (context, index) {
+ Group group = snapshot.data![index];
+ print("Members: ${group.members}");
+ print("User ID: ${_auth.currentUser!.uid}");
+ return FutureBuilder(
+ future: _db.getUserById(group.members.firstWhere(
+ (m) => m != _auth.currentUser!.uid,
+ orElse: () => group.createdBy)),
+ builder: (context, snapshot) {
+ if (snapshot.hasData) {
+ return ChatItem(
+ title: snapshot.data!.displayName,
+ subtitle: group.recentMessage?.messageText ?? "",
+ timestamp:
+ group.recentMessage?.sentAt ?? DateTime.now(),
+ avatarURL: snapshot.data!.photoUrl,
+ onTap: () {
+ Navigator.of(context).pushNamed(
+ '/chat',
+ arguments: [group, snapshot.data!],
+ );
+ },
+ );
+ } else if (snapshot.hasError) {
+ return ChatItem(
+ title: "Usuario desconocido",
+ subtitle: group.recentMessage?.messageText ?? "",
+ timestamp: DateTime.now(),
+ onTap: () {
+ Navigator.of(context).pushNamed(
+ '/chat',
+ arguments: [group, snapshot.data!],
+ );
+ },
+ );
+ }
+ return const Center(child: CircularProgressIndicator());
+ });
+ },
+ );
+ } else if (snapshot.hasError) {
+ print("Error: ${snapshot.error}");
+ return const Center(child: Text('Hubo un error'));
+ }
+ return const Center(child: CircularProgressIndicator());
+ }),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock index 28080bd..9039956 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + custom_clippers: + dependency: "direct main" + description: + name: custom_clippers + sha256: "2b83bb29ccbbd7d2d39220ec0361becb8ab545c3a0cb295979e3450bdad6034b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" email_validator: dependency: "direct main" description: @@ -328,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.3" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 90b0bf8..03c2aa2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: firebase_messaging: ^14.6.1 firebase_storage: ^11.2.1 cloud_firestore: ^4.7.1 + custom_clippers: ^2.0.0 + intl: ^0.18.1 dev_dependencies: flutter_test: |