aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2023-05-22 00:09:43 -0600
committerIván Ávalos <avalos@disroot.org>2023-05-22 00:09:43 -0600
commit786267d0ebb337ad5b4f1e528fdd4c23731e0606 (patch)
tree6e696aeae7fe14244b555da9815ecd19d0b92b14
parent41b1ed94d6964e3fcedf427bbd323861b03fd1de (diff)
downloadlinkchat-786267d0ebb337ad5b4f1e528fdd4c23731e0606.tar.gz
linkchat-786267d0ebb337ad5b4f1e528fdd4c23731e0606.tar.bz2
linkchat-786267d0ebb337ad5b4f1e528fdd4c23731e0606.zip
Se implementa funcionalidad básica de chat
-rw-r--r--lib/firebase/auth.dart16
-rw-r--r--lib/firebase/database.dart74
-rw-r--r--lib/models/chat.dart26
-rw-r--r--lib/models/favorito.dart16
-rw-r--r--lib/models/group.dart46
-rw-r--r--lib/models/mensaje.dart30
-rw-r--r--lib/models/message.dart29
-rw-r--r--lib/models/user.dart25
-rw-r--r--lib/routes.dart6
-rw-r--r--lib/screens/chat_screen.dart97
-rw-r--r--lib/screens/dashboard_screen.dart6
-rw-r--r--lib/screens/new_chat_screen.dart99
-rw-r--r--lib/widgets/active_chats.dart42
-rw-r--r--lib/widgets/chat_bottom_sheet.dart52
-rw-r--r--lib/widgets/chat_bubble.dart83
-rw-r--r--lib/widgets/chat_item.dart113
-rw-r--r--lib/widgets/recent_chats.dart87
-rw-r--r--pubspec.lock16
-rw-r--r--pubspec.yaml2
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: